Merge branch 'main' into stmt-builder-metrics

This commit is contained in:
srikanthccv 2025-06-03 01:15:09 +05:30
commit ded03c04d9
181 changed files with 46133 additions and 1790 deletions

1
.gitignore vendored
View File

@ -66,6 +66,7 @@ e2e/.auth
# go # go
vendor/ vendor/
**/main/** **/main/**
__debug_bin**
# git-town # git-town
.git-branches.toml .git-branches.toml

View File

@ -207,3 +207,11 @@ emailing:
key_file_path: key_file_path:
# The path to the certificate file. # The path to the certificate file.
cert_file_path: cert_file_path:
##################### Sharder (experimental) #####################
sharder:
# Specifies the sharder provider to use.
provider: noop
single:
# The org id to which this instance belongs to.
org_id: org_id

View File

@ -3,13 +3,15 @@ package httplicensing
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/SigNoz/signoz/ee/query-service/constants"
"time" "time"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore" "github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/featuretypes" "github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes" "github.com/SigNoz/signoz/pkg/types/licensetypes"
@ -19,23 +21,31 @@ import (
) )
type provider struct { type provider struct {
store licensetypes.Store store licensetypes.Store
zeus zeus.Zeus zeus zeus.Zeus
config licensing.Config config licensing.Config
settings factory.ScopedProviderSettings settings factory.ScopedProviderSettings
stopChan chan struct{} orgGetter organization.Getter
stopChan chan struct{}
} }
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] { func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) { return 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) return New(ctx, providerSettings, config, store, zeus, orgGetter)
}) })
} }
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus) (licensing.Licensing, error) { func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing") settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
licensestore := sqllicensingstore.New(sqlstore) licensestore := sqllicensingstore.New(sqlstore)
return &provider{store: licensestore, zeus: zeus, config: config, settings: settings, stopChan: make(chan struct{})}, nil return &provider{
store: licensestore,
zeus: zeus,
config: config,
settings: settings,
orgGetter: orgGetter,
stopChan: make(chan struct{}),
}, nil
} }
func (provider *provider) Start(ctx context.Context) error { func (provider *provider) Start(ctx context.Context) error {
@ -67,13 +77,13 @@ func (provider *provider) Stop(ctx context.Context) error {
} }
func (provider *provider) Validate(ctx context.Context) error { func (provider *provider) Validate(ctx context.Context) error {
organizations, err := provider.store.ListOrganizations(ctx) organizations, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil { if err != nil {
return err return err
} }
for _, organizationID := range organizations { for _, organization := range organizations {
err := provider.Refresh(ctx, organizationID) err := provider.Refresh(ctx, organization.ID)
if err != nil { if err != nil {
return err return err
} }
@ -168,6 +178,11 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err return err
} }
err = provider.InitFeatures(ctx, activeLicense.Features)
if err != nil {
return err
}
return nil return nil
} }

View File

@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore" "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/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes" "github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
@ -82,31 +81,6 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
return nil 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 { func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store. _, err := store.
sqlstore. sqlstore.

View File

@ -96,13 +96,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first // note: add ee override methods first
// 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.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
// invite
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
// paid plans specific routes // 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)
@ -114,9 +108,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost) router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v3 // v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost) router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut) router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)

View File

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

View File

@ -20,6 +20,7 @@ import (
"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"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
@ -113,6 +114,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.SQLStore,
serverOptions.SigNoz.TelemetryStore, serverOptions.SigNoz.TelemetryStore,
serverOptions.SigNoz.Prometheus, serverOptions.SigNoz.Prometheus,
serverOptions.SigNoz.Modules.OrgGetter,
) )
if err != nil { if err != nil {
@ -157,7 +159,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
} }
// start the usagemanager // start the usagemanager
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.Organization) usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.OrgGetter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -225,7 +227,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
&opAmpModel.AllAgents, agentConfMgr, &opAmpModel.AllAgents, agentConfMgr,
) )
orgs, err := apiHandler.Signoz.Modules.Organization.GetAll(context.Background()) orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -240,11 +242,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
} }
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) { func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
r := baseapp.NewRouter() r := baseapp.NewRouter()
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap) r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap) r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(), 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,
@ -275,8 +276,8 @@ 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(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap) r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap) r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(), 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,
@ -297,6 +298,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterMessagingQueuesRoutes(r, am) apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am) apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am) apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
c := cors.New(cors.Options{ c := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, AllowedOrigins: []string{"*"},
@ -450,6 +452,7 @@ func makeRulesManager(
sqlstore sqlstore.SQLStore, sqlstore sqlstore.SQLStore,
telemetryStore telemetrystore.TelemetryStore, telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus, prometheus prometheus.Prometheus,
orgGetter organization.Getter,
) (*baserules.Manager, error) { ) (*baserules.Manager, error) {
// create manager opts // create manager opts
managerOpts := &baserules.ManagerOptions{ managerOpts := &baserules.ManagerOptions{
@ -465,6 +468,7 @@ func makeRulesManager(
PrepareTestRuleFunc: rules.TestNotification, PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager, Alertmanager: alertmanager,
SQLStore: sqlstore, SQLStore: sqlstore,
OrgGetter: orgGetter,
} }
// create Manager // create Manager

View File

@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/config/fileprovider" "github.com/SigNoz/signoz/pkg/config/fileprovider"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
pkglicensing "github.com/SigNoz/signoz/pkg/licensing" pkglicensing "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
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"
@ -133,8 +134,8 @@ func main() {
zeus.Config(), zeus.Config(),
httpzeus.NewProviderFactory(), httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3), licensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] { func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus) return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
}, },
signoz.NewEmailingProviderFactories(), signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(), signoz.NewCacheProviderFactories(),

View File

@ -41,16 +41,16 @@ type Manager struct {
zeus zeus.Zeus zeus zeus.Zeus
organizationModule organization.Module orgGetter organization.Getter
} }
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, organizationModule organization.Module) (*Manager, error) { func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter) (*Manager, error) {
m := &Manager{ m := &Manager{
clickhouseConn: clickhouseConn, clickhouseConn: clickhouseConn,
licenseService: licenseService, 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
zeus: zeus, zeus: zeus,
organizationModule: organizationModule, orgGetter: orgGetter,
} }
return m, nil return m, nil
} }
@ -74,8 +74,7 @@ func (lm *Manager) Start(ctx context.Context) error {
return nil return nil
} }
func (lm *Manager) UploadUsage(ctx context.Context) { func (lm *Manager) UploadUsage(ctx context.Context) {
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
organizations, err := lm.organizationModule.GetAll(context.Background())
if err != nil { if err != nil {
zap.L().Error("failed to get organizations", zap.Error(err)) zap.L().Error("failed to get organizations", zap.Error(err))
return return

View File

@ -28,6 +28,7 @@ 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';
import { CompatRouter } from 'react-router-dom-v5-compat'; import { CompatRouter } from 'react-router-dom-v5-compat';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { Userpilot } from 'userpilot'; import { Userpilot } from 'userpilot';
import { extractDomain } from 'utils/app'; import { extractDomain } from 'utils/app';
@ -171,11 +172,13 @@ function App(): JSX.Element {
user && user &&
!!user.email !!user.email
) { ) {
// either the active API returns error with 404 or 501 and if it returns a terminated license means it's on basic plan
const isOnBasicPlan = const isOnBasicPlan =
activeLicenseFetchError && (activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes( [StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(), activeLicenseFetchError?.getHttpStatusCode(),
); )) ||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
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) {
@ -190,6 +193,10 @@ function App(): JSX.Element {
updatedRoutes = updatedRoutes.filter( updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING, (route) => route?.path !== ROUTES.BILLING,
); );
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
}
} }
// always add support route for cloud users // always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE]; updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview'; import Overview from 'container/LogDetailedView/Overview';
import { import {
aggregateAttributesResourcesToString, aggregateAttributesResourcesToString,
escapeHtml,
removeEscapeCharacters, removeEscapeCharacters,
unescapeString, unescapeString,
} from 'container/LogDetailedView/utils'; } from 'container/LogDetailedView/utils';
@ -118,7 +119,7 @@ function LogDetail({
const htmlBody = useMemo( const htmlBody = useMemo(
() => ({ () => ({
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(unescapeString(log?.body || ''), { dompurify.sanitize(unescapeString(escapeHtml(log?.body || '')), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}), }),
), ),

View File

@ -7,7 +7,7 @@ import cx from 'classnames';
import LogDetail from 'components/LogDetail'; import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants'; import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { unescapeString } from 'container/LogDetailedView/utils'; import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types'; import { FontSize } from 'container/OptionsMenu/types';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
@ -58,7 +58,7 @@ function LogGeneralField({
const html = useMemo( const html = useMemo(
() => ({ () => ({
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(unescapeString(fieldValue), { dompurify.sanitize(unescapeString(escapeHtml(fieldValue)), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}), }),
), ),

View File

@ -5,7 +5,7 @@ import { DrawerProps } from 'antd';
import LogDetail from 'components/LogDetail'; import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants'; import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { unescapeString } from 'container/LogDetailedView/utils'; import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext'; import LogsExplorerContext from 'container/LogsExplorerContext';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
@ -177,7 +177,7 @@ function RawLogView({
const html = useMemo( const html = useMemo(
() => ({ () => ({
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(unescapeString(text), { dompurify.sanitize(unescapeString(escapeHtml(text)), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}), }),
), ),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,7 @@ import { executeSearchQueries } from '../utils';
describe('executeSearchQueries', () => { describe('executeSearchQueries', () => {
const firstDashboard: Dashboard = { const firstDashboard: Dashboard = {
id: 11111, id: uuid(),
uuid: uuid(),
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
createdBy: '', createdBy: '',
@ -18,8 +17,7 @@ describe('executeSearchQueries', () => {
}, },
}; };
const secondDashboard: Dashboard = { const secondDashboard: Dashboard = {
id: 22222, id: uuid(),
uuid: uuid(),
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
createdBy: '', createdBy: '',
@ -30,8 +28,7 @@ describe('executeSearchQueries', () => {
}, },
}; };
const thirdDashboard: Dashboard = { const thirdDashboard: Dashboard = {
id: 333333, id: uuid(),
uuid: uuid(),
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
createdBy: '', createdBy: '',

View File

@ -59,7 +59,7 @@ export function DeleteButton({
onClick: (e) => { onClick: (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
deleteDashboardMutation.mutateAsync(undefined, { deleteDashboardMutation.mutate(undefined, {
onSuccess: () => { onSuccess: () => {
notifications.success({ notifications.success({
message: t('dashboard:delete_dashboard_success', { message: t('dashboard:delete_dashboard_success', {

View File

@ -14,7 +14,7 @@ export const generateSearchData = (
dashboards.forEach((dashboard) => { dashboards.forEach((dashboard) => {
dashboardSearchData.push({ dashboardSearchData.push({
id: dashboard.uuid, id: dashboard.id,
title: dashboard.data.title, title: dashboard.data.title,
description: dashboard.data.description, description: dashboard.data.description,
tags: dashboard.data.tags || [], tags: dashboard.data.tags || [],

View File

@ -21,8 +21,10 @@ import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { DataType } from '../TableView'; import { DataType } from '../TableView';
import { import {
escapeHtml,
filterKeyForField, filterKeyForField,
jsonToDataNodes, jsonToDataNodes,
parseFieldValue,
recursiveParseJSON, recursiveParseJSON,
removeEscapeCharacters, removeEscapeCharacters,
unescapeString, unescapeString,
@ -85,7 +87,7 @@ export function TableViewActions(
record.field === 'body' record.field === 'body'
? { ? {
__html: convert.toHtml( __html: convert.toHtml(
dompurify.sanitize(unescapeString(record.value), { dompurify.sanitize(unescapeString(escapeHtml(record.value)), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS], FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}), }),
), ),
@ -155,7 +157,11 @@ export function TableViewActions(
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} /> <ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
) )
} }
onClick={onClickHandler(OPERATORS['='], fieldFilterKey, fieldData.value)} onClick={onClickHandler(
OPERATORS['='],
fieldFilterKey,
parseFieldValue(fieldData.value),
)}
/> />
</Tooltip> </Tooltip>
<Tooltip title="Filter out value"> <Tooltip title="Filter out value">
@ -171,7 +177,7 @@ export function TableViewActions(
onClick={onClickHandler( onClick={onClickHandler(
OPERATORS['!='], OPERATORS['!='],
fieldFilterKey, fieldFilterKey,
fieldData.value, parseFieldValue(fieldData.value),
)} )}
/> />
</Tooltip> </Tooltip>

View File

@ -259,6 +259,24 @@ export const getDataTypes = (value: unknown): DataTypes => {
return determineType(value); return determineType(value);
}; };
// prevent html rendering in the value
export const escapeHtml = (unsafe: string): string =>
unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// parse field value to remove escaping characters
export const parseFieldValue = (value: string): string => {
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make // now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
// the log line readable // the log line readable
export const removeEscapeCharacters = (str: string): string => export const removeEscapeCharacters = (str: string): string =>

View File

@ -28,16 +28,12 @@ import LogsExplorerTable from 'container/LogsExplorerTable';
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import useClickOutside from 'hooks/useClickOutside'; import useClickOutside from 'hooks/useClickOutside';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
@ -98,7 +94,6 @@ function LogsExplorerViews({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
chartQueryKeyRef: MutableRefObject<any>; chartQueryKeyRef: MutableRefObject<any>;
}): JSX.Element { }): JSX.Element {
const { notifications } = useNotifications();
const { safeNavigate } = useSafeNavigate(); const { safeNavigate } = useSafeNavigate();
// this is to respect the panel type present in the URL rather than defaulting it to list always. // this is to respect the panel type present in the URL rather than defaulting it to list always.
@ -141,8 +136,6 @@ function LogsExplorerViews({
const [queryId, setQueryId] = useState<string>(v4()); const [queryId, setQueryId] = useState<string>(v4());
const [queryStats, setQueryStats] = useState<WsDataEvent>(); const [queryStats, setQueryStats] = useState<WsDataEvent>();
const handleAxisError = useAxiosError();
const listQuery = useMemo(() => { const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null; if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
@ -396,11 +389,6 @@ function LogsExplorerViews({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload]); }, [data?.payload]);
const {
mutate: updateDashboard,
isLoading: isUpdateDashboardLoading,
} = useUpdateDashboard();
const getUpdatedQueryForExport = useCallback((): Query => { const getUpdatedQueryForExport = useCallback((): Query => {
const updatedQuery = cloneDeep(currentQuery); const updatedQuery = cloneDeep(currentQuery);
@ -424,68 +412,22 @@ function LogsExplorerViews({
? getUpdatedQueryForExport() ? getUpdatedQueryForExport()
: exportDefaultQuery; : exportDefaultQuery;
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
dashboard,
query,
widgetId,
panelTypeParam,
options.selectColumns,
);
logEvent('Logs Explorer: Add to dashboard successful', { logEvent('Logs Explorer: Add to dashboard successful', {
panelType, panelType,
isNewDashboard, isNewDashboard,
dashboardName: dashboard?.data?.title, dashboardName: dashboard?.data?.title,
}); });
updateDashboard(updatedDashboard, { const dashboardEditView = generateExportToDashboardLink({
onSuccess: (data) => { query,
if (data.error) { panelType: panelTypeParam,
const message = dashboardId: dashboard.id,
data.error === 'feature usage exceeded' ? ( widgetId,
<span>
Panel limit exceeded for {DataSource.LOGS} in community edition. Please
checkout our paid plans{' '}
<a
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
rel="noreferrer noopener"
target="_blank"
>
here
</a>
</span>
) : (
data.error
);
notifications.error({
message,
});
return;
}
const dashboardEditView = generateExportToDashboardLink({
query,
panelType: panelTypeParam,
dashboardId: data.payload?.uuid || '',
widgetId,
});
safeNavigate(dashboardEditView);
},
onError: handleAxisError,
}); });
safeNavigate(dashboardEditView);
}, },
[ [getUpdatedQueryForExport, exportDefaultQuery, safeNavigate, panelType],
getUpdatedQueryForExport,
exportDefaultQuery,
options.selectColumns,
safeNavigate,
notifications,
panelType,
updateDashboard,
handleAxisError,
],
); );
useEffect(() => { useEffect(() => {
@ -811,7 +753,6 @@ function LogsExplorerViews({
<ExplorerOptionWrapper <ExplorerOptionWrapper
disabled={!stagedQuery} disabled={!stagedQuery}
query={exportDefaultQuery} query={exportDefaultQuery}
isLoading={isUpdateDashboardLoading}
onExport={handleExport} onExport={handleExport}
sourcepage={DataSource.LOGS} sourcepage={DataSource.LOGS}
/> />

View File

@ -2,18 +2,12 @@ import './Explorer.styles.scss';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Switch } from 'antd'; import { Switch } from 'antd';
import axios from 'axios';
import { LOCALSTORAGE } from 'constants/localStorage';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper'; import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import { useOptionsMenu } from 'container/OptionsMenu';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions'; import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@ -39,13 +33,6 @@ function Explorer(): JSX.Element {
currentQuery, currentQuery,
} = useQueryBuilder(); } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate(); const { safeNavigate } = useSafeNavigate();
const { notifications } = useNotifications();
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.METRICS_LIST_OPTIONS,
dataSource: DataSource.METRICS,
aggregateOperator: 'noop',
});
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const isOneChartPerQueryEnabled = const isOneChartPerQueryEnabled =
@ -86,59 +73,16 @@ function Explorer(): JSX.Element {
const widgetId = uuid(); const widgetId = uuid();
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery( const dashboardEditView = generateExportToDashboardLink({
dashboard, query: queryToExport || exportDefaultQuery,
queryToExport || exportDefaultQuery, panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard.id,
widgetId, widgetId,
PANEL_TYPES.TIME_SERIES,
options.selectColumns,
);
updateDashboard(updatedDashboard, {
onSuccess: (data) => {
if (data.error) {
const message =
data.error === 'feature usage exceeded' ? (
<span>
Panel limit exceeded for {DataSource.METRICS} in community edition.
Please checkout our paid plans{' '}
<a
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
rel="noreferrer noopener"
target="_blank"
>
here
</a>
</span>
) : (
data.error
);
notifications.error({
message,
});
return;
}
const dashboardEditView = generateExportToDashboardLink({
query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: data.payload?.uuid || '',
widgetId,
});
safeNavigate(dashboardEditView);
},
onError: (error) => {
if (axios.isAxiosError(error)) {
notifications.error({
message: error.message,
});
}
},
}); });
safeNavigate(dashboardEditView);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps [exportDefaultQuery, safeNavigate],
[exportDefaultQuery, notifications, updateDashboard],
); );
const splitedQueries = useMemo( const splitedQueries = useMemo(
@ -201,7 +145,6 @@ function Explorer(): JSX.Element {
<ExplorerOptionWrapper <ExplorerOptionWrapper
disabled={!stagedQuery} disabled={!stagedQuery}
query={exportDefaultQuery} query={exportDefaultQuery}
isLoading={isLoading}
sourcepage={DataSource.METRICS} sourcepage={DataSource.METRICS}
onExport={handleExport} onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery} isOneChartPerQuery={showOneChartPerQuery}

View File

@ -12,7 +12,6 @@ import {
Typography, Typography,
} from 'antd'; } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -44,11 +43,8 @@ import { FullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
Dashboard, import { Props } from 'types/api/dashboard/update';
DashboardData,
IDashboardVariable,
} from 'types/api/dashboard/getAll';
import { ROLES, USER_ROLES } from 'types/roles'; import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission'; import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -65,10 +61,9 @@ interface DashboardDescriptionProps {
export function sanitizeDashboardData( export function sanitizeDashboardData(
selectedData: DashboardData, selectedData: DashboardData,
): Omit<DashboardData, 'uuid'> { ): DashboardData {
if (!selectedData?.variables) { if (!selectedData?.variables) {
const { uuid, ...rest } = selectedData; return selectedData;
return rest;
} }
const updatedVariables = Object.entries(selectedData.variables).reduce( const updatedVariables = Object.entries(selectedData.variables).reduce(
@ -80,9 +75,8 @@ export function sanitizeDashboardData(
{} as Record<string, IDashboardVariable>, {} as Record<string, IDashboardVariable>,
); );
const { uuid, ...restData } = selectedData;
return { return {
...restData, ...selectedData,
variables: updatedVariables, variables: updatedVariables,
}; };
} }
@ -108,7 +102,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const selectedData = selectedDashboard const selectedData = selectedDashboard
? { ? {
...selectedDashboard.data, ...selectedDashboard.data,
uuid: selectedDashboard.uuid, uuid: selectedDashboard.id,
} }
: ({} as DashboardData); : ({} as DashboardData);
@ -162,7 +156,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setSelectedRowWidgetId(null); setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true); handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', { logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length, numberOfPanels: selectedDashboard?.data.widgets?.length,
}); });
@ -178,8 +172,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
if (!selectedDashboard) { if (!selectedDashboard) {
return; return;
} }
const updatedDashboard = { const updatedDashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
title: updatedTitle, title: updatedTitle,
@ -191,13 +186,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
message: 'Dashboard renamed successfully', message: 'Dashboard renamed successfully',
}); });
setIsRenameDashboardOpen(false); setIsRenameDashboardOpen(false);
if (updatedDashboard.payload) if (updatedDashboard.data) setSelectedDashboard(updatedDashboard.data);
setSelectedDashboard(updatedDashboard.payload);
}, },
onError: () => { onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
setIsRenameDashboardOpen(true); setIsRenameDashboardOpen(true);
}, },
}); });
@ -251,8 +242,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
} }
} }
const updatedDashboard: Dashboard = { const updatedDashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
layout: [ layout: [
@ -279,28 +271,21 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
}, },
], ],
}, },
uuid: selectedDashboard.uuid,
}; };
updateDashboardMutation.mutate(updatedDashboard, { updateDashboardMutation.mutate(updatedDashboard, {
// eslint-disable-next-line sonarjs/no-identical-functions // eslint-disable-next-line sonarjs/no-identical-functions
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) { if (updatedDashboard.data) {
if (updatedDashboard.payload.data.layout) if (updatedDashboard.data.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout)); setLayouts(sortLayout(updatedDashboard.data.data.layout));
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); setPanelMap(updatedDashboard.data?.data?.panelMap || {});
} }
setIsPanelNameModalOpen(false); setIsPanelNameModalOpen(false);
setSectionName(DEFAULT_ROW_NAME); setSectionName(DEFAULT_ROW_NAME);
}, },
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}); });
} }
@ -445,7 +430,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DeleteButton <DeleteButton
createdBy={selectedDashboard?.createdBy || ''} createdBy={selectedDashboard?.createdBy || ''}
name={selectedDashboard?.data.title || ''} name={selectedDashboard?.data.title || ''}
id={String(selectedDashboard?.uuid) || ''} id={String(selectedDashboard?.id) || ''}
isLocked={isDashboardLocked} isLocked={isDashboardLocked}
routeToListPage routeToListPage
/> />

View File

@ -1,10 +1,8 @@
import './GeneralSettings.styles.scss'; import './GeneralSettings.styles.scss';
import { Col, Input, Select, Space, Typography } from 'antd'; import { Col, Input, Select, Space, Typography } from 'antd';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
@ -38,14 +36,12 @@ function GeneralDashboardSettings(): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { notifications } = useNotifications();
const onSaveHandler = (): void => { const onSaveHandler = (): void => {
if (!selectedDashboard) return; if (!selectedDashboard) return;
updateDashboardMutation.mutateAsync( updateDashboardMutation.mutate(
{ {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
description: updatedDescription, description: updatedDescription,
@ -56,15 +52,11 @@ function GeneralDashboardSettings(): JSX.Element {
}, },
{ {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) { if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
} }
}, },
onError: () => { onError: () => {},
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}, },
); );
}; };

View File

@ -171,7 +171,8 @@ function VariablesSetting({
updateMutation.mutateAsync( updateMutation.mutateAsync(
{ {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
variables: updatedVariablesData, variables: updatedVariablesData,
@ -179,18 +180,13 @@ function VariablesSetting({
}, },
{ {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) { if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
notifications.success({ notifications.success({
message: t('variable_updated_successfully'), message: t('variable_updated_successfully'),
}); });
} }
}, },
onError: () => {
notifications.error({
message: t('error_while_updating_variable'),
});
},
}, },
); );
}; };

View File

@ -127,7 +127,7 @@ function QuerySection({
panelType: selectedWidget.panelTypes, panelType: selectedWidget.panelTypes,
queryType: currentQuery.queryType, queryType: currentQuery.queryType,
widgetId: selectedWidget.id, widgetId: selectedWidget.id,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
isNewPanel, isNewPanel,
}); });

View File

@ -17,7 +17,6 @@ import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
@ -41,10 +40,10 @@ import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { import {
ColumnUnit, ColumnUnit,
Dashboard,
LegendPosition, LegendPosition,
Widgets, Widgets,
} from 'types/api/dashboard/getAll'; } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
@ -141,7 +140,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
if (!logEventCalledRef.current) { if (!logEventCalledRef.current) {
logEvent('Panel Edit: Page visited', { logEvent('Panel Edit: Page visited', {
panelType: selectedWidget?.panelTypes, panelType: selectedWidget?.panelTypes,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: selectedWidget?.id, widgetId: selectedWidget?.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
isNewPanel: !!isWidgetNotPresent, isNewPanel: !!isWidgetNotPresent,
@ -345,8 +344,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
return { selectedWidget, preWidgets, afterWidgets }; return { selectedWidget, preWidgets, afterWidgets };
}, [selectedDashboard, query]); }, [selectedDashboard, query]);
const handleError = useAxiosError();
// this loading state is to take care of mismatch in the responses for table and other panels // this loading state is to take care of mismatch in the responses for table and other panels
// hence while changing the query contains the older value and the processing logic fails // hence while changing the query contains the older value and the processing logic fails
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false); const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
@ -470,9 +467,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
updatedLayout = newLayoutItem; updatedLayout = newLayoutItem;
} }
const dashboard: Dashboard = { const dashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
uuid: selectedDashboard.uuid,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
widgets: isNewDashboard widgets: isNewDashboard
@ -540,15 +537,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
}; };
updateDashboardMutation.mutateAsync(dashboard, { updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => { onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null); setSelectedRowWidgetId(null);
setSelectedDashboard(dashboard); setSelectedDashboard(updatedDashboard.data);
setToScrollWidgetId(selectedWidget?.id || ''); setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({ safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }), pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
}); });
}, },
onError: handleError,
}); });
}, [ }, [
selectedDashboard, selectedDashboard,
@ -562,7 +558,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
currentQuery, currentQuery,
preWidgets, preWidgets,
updateDashboardMutation, updateDashboardMutation,
handleError,
widgets, widgets,
setSelectedDashboard, setSelectedDashboard,
setToScrollWidgetId, setToScrollWidgetId,
@ -601,7 +596,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
logEvent('Panel Edit: Save changes', { logEvent('Panel Edit: Save changes', {
panelType: selectedWidget.panelTypes, panelType: selectedWidget.panelTypes,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: selectedWidget.id, widgetId: selectedWidget.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
queryType: currentQuery.queryType, queryType: currentQuery.queryType,

View File

@ -26,6 +26,7 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app'; import { checkVersionState } from 'utils/app';
@ -301,10 +302,11 @@ function SideNav(): JSX.Element {
} }
const isOnBasicPlan = const isOnBasicPlan =
activeLicenseFetchError && (activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes( [StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(), activeLicenseFetchError?.getHttpStatusCode(),
); )) ||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) { if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) {
updatedMenuItems = updatedMenuItems.filter( updatedMenuItems = updatedMenuItems.filter(
@ -353,6 +355,7 @@ function SideNav(): JSX.Element {
t, t,
user.role, user.role,
activeLicenseFetchError, activeLicenseFetchError,
activeLicense?.status,
]); ]);
return ( return (

View File

@ -1,15 +1,23 @@
import deleteDashboard from 'api/dashboard/delete'; import deleteDashboard from 'api/v1/dashboards/id/delete';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMutation, UseMutationResult } from 'react-query'; import { useMutation, UseMutationResult } from 'react-query';
import { PayloadProps } from 'types/api/dashboard/delete'; import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
export const useDeleteDashboard = ( export const useDeleteDashboard = (
id: string, id: string,
): UseMutationResult<PayloadProps, unknown, void, unknown> => ): UseMutationResult<SuccessResponseV2<null>, APIError, void, unknown> => {
useMutation({ const { showErrorModal } = useErrorModal();
return useMutation<SuccessResponseV2<null>, APIError>({
mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD, mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD,
mutationFn: () => mutationFn: () =>
deleteDashboard({ deleteDashboard({
uuid: id, id,
}), }),
onError: (error: APIError) => {
showErrorModal(error);
},
}); });
};

View File

@ -1,10 +1,22 @@
import { getAllDashboardList } from 'api/dashboard/getAll'; import getAll from 'api/v1/dashboards/getAll';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useQuery, UseQueryResult } from 'react-query'; import { useQuery, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
export const useGetAllDashboard = (): UseQueryResult<Dashboard[], unknown> => export const useGetAllDashboard = (): UseQueryResult<
useQuery<Dashboard[]>({ SuccessResponseV2<Dashboard[]>,
queryFn: getAllDashboardList, APIError
> => {
const { showErrorModal } = useErrorModal();
return useQuery<SuccessResponseV2<Dashboard[]>, APIError>({
queryFn: getAll,
onError: (error) => {
showErrorModal(error);
},
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS, queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
}); });
};

View File

@ -1,25 +1,31 @@
import update from 'api/dashboard/update'; import update from 'api/v1/dashboards/id/update';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMutation, UseMutationResult } from 'react-query'; import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update'; import { Props } from 'types/api/dashboard/update';
import APIError from 'types/api/error';
export const useUpdateDashboard = (): UseUpdateDashboard => { export const useUpdateDashboard = (): UseUpdateDashboard => {
const { updatedTimeRef } = useDashboard(); const { updatedTimeRef } = useDashboard();
const { showErrorModal } = useErrorModal();
return useMutation(update, { return useMutation(update, {
onSuccess: (data) => { onSuccess: (data) => {
if (data.payload) { if (data.data) {
updatedTimeRef.current = dayjs(data.payload.updatedAt); updatedTimeRef.current = dayjs(data.data.updatedAt);
} }
}, },
onError: (error) => {
showErrorModal(error);
},
}); });
}; };
type UseUpdateDashboard = UseMutationResult< type UseUpdateDashboard = UseMutationResult<
SuccessResponse<Dashboard> | ErrorResponse, SuccessResponseV2<Dashboard>,
unknown, APIError,
Props, Props,
unknown unknown
>; >;

View File

@ -38,7 +38,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
logEvent('Panel Edit: Create alert', { logEvent('Panel Edit: Create alert', {
panelType: widget.panelTypes, panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title, dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: widget.id, widgetId: widget.id,
queryType: widget.query.queryType, queryType: widget.query.queryType,
}); });
@ -47,7 +47,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
action: MenuItemKeys.CreateAlerts, action: MenuItemKeys.CreateAlerts,
panelType: widget.panelTypes, panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title, dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: widget.id, widgetId: widget.id,
queryType: widget.query.queryType, queryType: widget.query.queryType,
}); });

View File

@ -3,8 +3,7 @@ export const dashboardSuccessResponse = {
status: 'success', status: 'success',
data: [ data: [
{ {
id: 1, id: '1',
uuid: '1',
createdAt: '2022-11-16T13:29:47.064874419Z', createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: null, createdBy: null,
updatedAt: '2024-05-21T06:41:30.546630961Z', updatedAt: '2024-05-21T06:41:30.546630961Z',
@ -23,8 +22,7 @@ export const dashboardSuccessResponse = {
}, },
}, },
{ {
id: 2, id: '2',
uuid: '2',
createdAt: '2022-11-16T13:20:47.064874419Z', createdAt: '2022-11-16T13:20:47.064874419Z',
createdBy: null, createdBy: null,
updatedAt: '2024-05-21T06:42:30.546630961Z', updatedAt: '2024-05-21T06:42:30.546630961Z',
@ -53,8 +51,7 @@ export const dashboardEmptyState = {
export const getDashboardById = { export const getDashboardById = {
status: 'success', status: 'success',
data: { data: {
id: 1, id: '1',
uuid: '1',
createdAt: '2022-11-16T13:29:47.064874419Z', createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: 'integration', createdBy: 'integration',
updatedAt: '2024-05-21T06:41:30.546630961Z', updatedAt: '2024-05-21T06:41:30.546630961Z',
@ -78,8 +75,7 @@ export const getDashboardById = {
export const getNonIntegrationDashboardById = { export const getNonIntegrationDashboardById = {
status: 'success', status: 'success',
data: { data: {
id: 1, id: '1',
uuid: '1',
createdAt: '2022-11-16T13:29:47.064874419Z', createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: 'thor', createdBy: 'thor',
updatedAt: '2024-05-21T06:41:30.546630961Z', updatedAt: '2024-05-21T06:41:30.546630961Z',

View File

@ -234,7 +234,6 @@ describe('dashboard list page', () => {
const firstDashboardData = dashboardSuccessResponse.data[0]; const firstDashboardData = dashboardSuccessResponse.data[0];
expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith( expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
id: firstDashboardData.uuid,
title: firstDashboardData.data.title, title: firstDashboardData.data.title,
createdAt: firstDashboardData.createdAt, createdAt: firstDashboardData.createdAt,
}), }),

View File

@ -19,9 +19,9 @@ function DashboardPage(): JSX.Element {
: 'Something went wrong'; : 'Something went wrong';
useEffect(() => { useEffect(() => {
const dashboardTitle = dashboardResponse.data?.data.title; const dashboardTitle = dashboardResponse.data?.data.data.title;
document.title = dashboardTitle || document.title; document.title = dashboardTitle || document.title;
}, [dashboardResponse.data?.data.title, isFetching]); }, [dashboardResponse.data?.data.data.title, isFetching]);
if (isError && !isFetching && errorMessage === ErrorType.NotFound) { if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
return <NotFound />; return <NotFound />;

View File

@ -4,7 +4,6 @@ import { FilterOutlined } from '@ant-design/icons';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Button, Card, Tabs, Tooltip } from 'antd'; import { Button, Card, Tabs, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import axios from 'axios';
import cx from 'classnames'; import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard'; import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QuickFilters from 'components/QuickFilters/QuickFilters'; import QuickFilters from 'components/QuickFilters/QuickFilters';
@ -19,13 +18,10 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/configs'; import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/configs';
import QuerySection from 'container/TracesExplorer/QuerySection'; import QuerySection from 'container/TracesExplorer/QuerySection';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { cloneDeep, isEmpty, set } from 'lodash-es'; import { cloneDeep, isEmpty, set } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
@ -40,8 +36,6 @@ import { ActionsWrapper, Container } from './styles';
import { getTabsItems } from './utils'; import { getTabsItems } from './utils';
function TracesExplorer(): JSX.Element { function TracesExplorer(): JSX.Element {
const { notifications } = useNotifications();
const { const {
currentQuery, currentQuery,
panelType, panelType,
@ -124,9 +118,7 @@ function TracesExplorer(): JSX.Element {
[currentQuery, updateAllQueriesOperators], [currentQuery, updateAllQueriesOperators],
); );
const { mutate: updateDashboard, isLoading } = useUpdateDashboard(); const getUpdatedQueryForExport = useCallback((): Query => {
const getUpdatedQueryForExport = (): Query => {
const updatedQuery = cloneDeep(currentQuery); const updatedQuery = cloneDeep(currentQuery);
set( set(
@ -136,7 +128,7 @@ function TracesExplorer(): JSX.Element {
); );
return updatedQuery; return updatedQuery;
}; }, [currentQuery, options.selectColumns]);
const handleExport = useCallback( const handleExport = useCallback(
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => { (dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
@ -153,65 +145,22 @@ function TracesExplorer(): JSX.Element {
? getUpdatedQueryForExport() ? getUpdatedQueryForExport()
: exportDefaultQuery; : exportDefaultQuery;
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
dashboard,
query,
widgetId,
panelTypeParam,
options.selectColumns,
);
logEvent('Traces Explorer: Add to dashboard successful', { logEvent('Traces Explorer: Add to dashboard successful', {
panelType, panelType,
isNewDashboard, isNewDashboard,
dashboardName: dashboard?.data?.title, dashboardName: dashboard?.data?.title,
}); });
updateDashboard(updatedDashboard, { const dashboardEditView = generateExportToDashboardLink({
onSuccess: (data) => { query,
if (data.error) { panelType: panelTypeParam,
const message = dashboardId: dashboard.id,
data.error === 'feature usage exceeded' ? ( widgetId,
<span>
Panel limit exceeded for {DataSource.TRACES} in community edition.
Please checkout our paid plans{' '}
<a
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
rel="noreferrer noopener"
target="_blank"
>
here
</a>
</span>
) : (
data.error
);
notifications.error({
message,
});
return;
}
const dashboardEditView = generateExportToDashboardLink({
query,
panelType: panelTypeParam,
dashboardId: data.payload?.uuid || '',
widgetId,
});
safeNavigate(dashboardEditView);
},
onError: (error) => {
if (axios.isAxiosError(error)) {
notifications.error({
message: error.message,
});
}
},
}); });
safeNavigate(dashboardEditView);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps [exportDefaultQuery, panelType, safeNavigate, getUpdatedQueryForExport],
[exportDefaultQuery, notifications, panelType, updateDashboard],
); );
useShareBuilderUrl(defaultQuery); useShareBuilderUrl(defaultQuery);
@ -282,11 +231,7 @@ function TracesExplorer(): JSX.Element {
<Container className="traces-explorer-views"> <Container className="traces-explorer-views">
<ActionsWrapper> <ActionsWrapper>
<ExportPanel <ExportPanel query={exportDefaultQuery} onExport={handleExport} />
query={exportDefaultQuery}
isLoading={isLoading}
onExport={handleExport}
/>
</ActionsWrapper> </ActionsWrapper>
<Tabs <Tabs
@ -299,7 +244,6 @@ function TracesExplorer(): JSX.Element {
<ExplorerOptionWrapper <ExplorerOptionWrapper
disabled={!stagedQuery} disabled={!stagedQuery}
query={exportDefaultQuery} query={exportDefaultQuery}
isLoading={isLoading}
sourcepage={DataSource.TRACES} sourcepage={DataSource.TRACES}
onExport={handleExport} onExport={handleExport}
/> />

View File

@ -1,14 +1,12 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import { Modal } from 'antd'; import { Modal } from 'antd';
import getDashboard from 'api/dashboard/get'; import getDashboard from 'api/v1/dashboards/id/get';
import lockDashboardApi from 'api/dashboard/lockDashboard'; import locked from 'api/v1/dashboards/id/lock';
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage'; import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useAxiosError from 'hooks/useAxiosError';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useTabVisibility from 'hooks/useTabFocus'; import useTabVisibility from 'hooks/useTabFocus';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
@ -18,6 +16,7 @@ import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined'; import isUndefined from 'lodash-es/isUndefined';
import omitBy from 'lodash-es/omitBy'; import omitBy from 'lodash-es/omitBy';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { import {
createContext, createContext,
PropsWithChildren, PropsWithChildren,
@ -36,7 +35,9 @@ import { Dispatch } from 'redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
@ -52,7 +53,10 @@ const DashboardContext = createContext<IDashboardContext>({
isDashboardLocked: false, isDashboardLocked: false,
handleToggleDashboardSlider: () => {}, handleToggleDashboardSlider: () => {},
handleDashboardLockToggle: () => {}, handleDashboardLockToggle: () => {},
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>, dashboardResponse: {} as UseQueryResult<
SuccessResponseV2<Dashboard>,
APIError
>,
selectedDashboard: {} as Dashboard, selectedDashboard: {} as Dashboard,
dashboardId: '', dashboardId: '',
layouts: [], layouts: [],
@ -116,6 +120,8 @@ export function DashboardProvider({
exact: true, exact: true,
}); });
const { showErrorModal } = useErrorModal();
// added extra checks here in case wrong values appear use the default values rather than empty dashboards // added extra checks here in case wrong values appear use the default values rather than empty dashboards
const supportedOrderColumnKeys = ['createdAt', 'updatedAt']; const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
@ -270,18 +276,24 @@ export function DashboardProvider({
setIsDashboardFetching(true); setIsDashboardFetching(true);
try { try {
return await getDashboard({ return await getDashboard({
uuid: dashboardId, id: dashboardId,
}); });
} catch (error) {
showErrorModal(error as APIError);
return;
} finally { } finally {
setIsDashboardFetching(false); setIsDashboardFetching(false);
} }
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onSuccess: (data) => { onError: (error) => {
const updatedDashboardData = transformDashboardVariables(data); showErrorModal(error as APIError);
},
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
const updatedDashboardData = transformDashboardVariables(data?.data);
const updatedDate = dayjs(updatedDashboardData.updatedAt); const updatedDate = dayjs(updatedDashboardData.updatedAt);
setIsDashboardLocked(updatedDashboardData?.isLocked || false); setIsDashboardLocked(updatedDashboardData?.locked || false);
// on first render // on first render
if (updatedTimeRef.current === null) { if (updatedTimeRef.current === null) {
@ -387,29 +399,25 @@ export function DashboardProvider({
setIsDashboardSlider(value); setIsDashboardSlider(value);
}; };
const handleError = useAxiosError(); const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
const { mutate: lockDashboard } = useMutation(lockDashboardApi, {
onSuccess: () => {
setIsDashboardSlider(false); setIsDashboardSlider(false);
setIsDashboardLocked(true); setIsDashboardLocked(props.lock);
}, },
onError: handleError, onError: (error) => {
}); showErrorModal(error as APIError);
const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, {
onSuccess: () => {
setIsDashboardLocked(false);
}, },
onError: handleError,
}); });
const handleDashboardLockToggle = async (value: boolean): Promise<void> => { const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
if (selectedDashboard) { if (selectedDashboard) {
if (value) { try {
lockDashboard(selectedDashboard); await lockDashboard({
} else { id: selectedDashboard.id,
unlockDashboard(selectedDashboard); lock: value,
});
} catch (error) {
showErrorModal(error as APIError);
} }
} }
}; };

View File

@ -1,6 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
export interface DashboardSortOrder { export interface DashboardSortOrder {
@ -19,7 +20,7 @@ export interface IDashboardContext {
isDashboardLocked: boolean; isDashboardLocked: boolean;
handleToggleDashboardSlider: (value: boolean) => void; handleToggleDashboardSlider: (value: boolean) => void;
handleDashboardLockToggle: (value: boolean) => void; handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<Dashboard, unknown>; dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined; selectedDashboard: Dashboard | undefined;
dashboardId: string; dashboardId: string;
layouts: Layout[]; layouts: Layout[];

View File

@ -1,11 +1,12 @@
import { Dashboard, DashboardData } from './getAll'; import { Dashboard } from './getAll';
export type Props = export type Props = {
| { title: Dashboard['data']['title'];
title: Dashboard['data']['title']; uploadedGrafana: boolean;
uploadedGrafana: boolean; version?: string;
version?: string; };
}
| { DashboardData: DashboardData; uploadedGrafana: boolean };
export type PayloadProps = Dashboard; export interface PayloadProps {
data: Dashboard;
status: string;
}

View File

@ -1,9 +1,10 @@
import { Dashboard } from './getAll'; import { Dashboard } from './getAll';
export type Props = { export type Props = {
uuid: Dashboard['uuid']; id: Dashboard['id'];
}; };
export interface PayloadProps { export interface PayloadProps {
status: 'success'; status: string;
data: null;
} }

View File

@ -1,7 +1,10 @@
import { Dashboard } from './getAll'; import { Dashboard } from './getAll';
export type Props = { export type Props = {
uuid: Dashboard['uuid']; id: Dashboard['id'];
}; };
export type PayloadProps = Dashboard; export interface PayloadProps {
data: Dashboard;
status: string;
}

View File

@ -9,8 +9,6 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { IField } from '../logs/fields'; import { IField } from '../logs/fields';
import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse';
export type PayloadProps = Dashboard[];
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
export type TVariableQueryType = typeof VariableQueryTypeArr[number]; export type TVariableQueryType = typeof VariableQueryTypeArr[number];
@ -50,14 +48,18 @@ export interface IDashboardVariable {
change?: boolean; change?: boolean;
} }
export interface Dashboard { export interface Dashboard {
id: number; id: string;
uuid: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
createdBy: string; createdBy: string;
updatedBy: string; updatedBy: string;
data: DashboardData; data: DashboardData;
isLocked?: boolean; locked?: boolean;
}
export interface PayloadProps {
data: Dashboard[];
status: string;
} }
export interface DashboardTemplate { export interface DashboardTemplate {
@ -69,7 +71,7 @@ export interface DashboardTemplate {
} }
export interface DashboardData { export interface DashboardData {
uuid?: string; // uuid?: string;
description?: string; description?: string;
tags?: string[]; tags?: string[];
name?: string; name?: string;

View File

@ -0,0 +1,11 @@
import { Dashboard } from './getAll';
export type Props = {
id: Dashboard['id'];
lock: boolean;
};
export interface PayloadProps {
data: null;
status: string;
}

View File

@ -1,8 +1,11 @@
import { Dashboard, DashboardData } from './getAll'; import { Dashboard, DashboardData } from './getAll';
export type Props = { export type Props = {
uuid: Dashboard['uuid']; id: Dashboard['id'];
data: DashboardData; data: DashboardData;
}; };
export type PayloadProps = Dashboard; export interface PayloadProps {
data: Dashboard;
status: string;
}

View File

@ -6,6 +6,7 @@ export enum LicenseEvent {
export enum LicenseStatus { export enum LicenseStatus {
SUSPENDED = 'SUSPENDED', SUSPENDED = 'SUSPENDED',
VALID = 'VALID', VALID = 'VALID',
INVALID = 'INVALID',
} }
export enum LicenseState { export enum LicenseState {

2
go.mod
View File

@ -26,7 +26,6 @@ require (
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/gosimple/slug v1.10.0
github.com/huandu/go-sqlbuilder v1.35.0 github.com/huandu/go-sqlbuilder v1.35.0
github.com/jackc/pgx/v5 v5.7.2 github.com/jackc/pgx/v5 v5.7.2
github.com/jmoiron/sqlx v1.3.4 github.com/jmoiron/sqlx v1.3.4
@ -138,7 +137,6 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gosimple/unidecode v1.0.0 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect

4
go.sum
View File

@ -450,10 +450,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosimple/slug v1.10.0 h1:3XbiQua1IpCdrvuntWvGBxVm+K99wCSxJjlxkP49GGQ=
github.com/gosimple/slug v1.10.0/go.mod h1:MICb3w495l9KNdZm+Xn5b6T2Hn831f9DMxiJ1r+bAjw=
github.com/gosimple/unidecode v1.0.0 h1:kPdvM+qy0tnk4/BrnkrbdJ82xe88xn7c9hcaipDz4dQ=
github.com/gosimple/unidecode v1.0.0/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=

View File

@ -67,23 +67,6 @@ func (store *config) Set(ctx context.Context, config *alertmanagertypes.Config,
}, opts...) }, opts...)
} }
func (store *config) ListOrgs(ctx context.Context) ([]string, error) {
var orgIDs []string
err := store.
sqlstore.
BunDB().
NewSelect().
Table("organizations").
ColumnExpr("id").
Scan(ctx, &orgIDs)
if err != nil {
return nil, err
}
return orgIDs, nil
}
func (store *config) CreateChannel(ctx context.Context, channel *alertmanagertypes.Channel, opts ...alertmanagertypes.StoreOption) error { func (store *config) CreateChannel(ctx context.Context, channel *alertmanagertypes.Channel, opts ...alertmanagertypes.StoreOption) error {
return store.wrap(ctx, func(ctx context.Context) error { return store.wrap(ctx, func(ctx context.Context) error {
if _, err := store. if _, err := store.

View File

@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerbatcher" "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerbatcher"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore" "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes" "github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
@ -57,16 +58,17 @@ type provider struct {
configStore alertmanagertypes.ConfigStore configStore alertmanagertypes.ConfigStore
batcher *alertmanagerbatcher.Batcher batcher *alertmanagerbatcher.Batcher
url *url.URL url *url.URL
orgGetter organization.Getter
orgID string orgID string
} }
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] { func NewFactory(sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
return factory.NewProviderFactory(factory.MustNewName("legacy"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) { return factory.NewProviderFactory(factory.MustNewName("legacy"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
return New(ctx, settings, config, sqlstore) return New(ctx, settings, config, sqlstore, orgGetter)
}) })
} }
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (*provider, error) { func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore, orgGetter organization.Getter) (*provider, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/legacyalertmanager") settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/legacyalertmanager")
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore) configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
@ -92,7 +94,7 @@ func (provider *provider) Start(ctx context.Context) error {
// For the first time, we need to get the orgID from the config store. // For the first time, we need to get the orgID from the config store.
// Since this is the legacy alertmanager, we get the first org from the store. // Since this is the legacy alertmanager, we get the first org from the store.
if provider.orgID == "" { if provider.orgID == "" {
orgIDs, err := provider.configStore.ListOrgs(ctx) orgIDs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil { if err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", err) provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", err)
continue continue
@ -103,7 +105,7 @@ func (provider *provider) Start(ctx context.Context) error {
continue continue
} }
provider.orgID = orgIDs[0] provider.orgID = orgIDs[0].ID.String()
} }
if err := provider.putAlerts(ctx, provider.orgID, alerts); err != nil { if err := provider.putAlerts(ctx, provider.orgID, alerts); err != nil {

View File

@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver" "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes" "github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
) )
@ -20,6 +21,9 @@ type Service struct {
// configStore is the config store for the alertmanager service // configStore is the config store for the alertmanager service
configStore alertmanagertypes.ConfigStore configStore alertmanagertypes.ConfigStore
// organization is the organization module for the alertmanager service
orgGetter organization.Getter
// settings is the settings for the alertmanager service // settings is the settings for the alertmanager service
settings factory.ScopedProviderSettings settings factory.ScopedProviderSettings
@ -30,11 +34,19 @@ type Service struct {
serversMtx sync.RWMutex serversMtx sync.RWMutex
} }
func New(ctx context.Context, settings factory.ScopedProviderSettings, config alertmanagerserver.Config, stateStore alertmanagertypes.StateStore, configStore alertmanagertypes.ConfigStore) *Service { func New(
ctx context.Context,
settings factory.ScopedProviderSettings,
config alertmanagerserver.Config,
stateStore alertmanagertypes.StateStore,
configStore alertmanagertypes.ConfigStore,
orgGetter organization.Getter,
) *Service {
service := &Service{ service := &Service{
config: config, config: config,
stateStore: stateStore, stateStore: stateStore,
configStore: configStore, configStore: configStore,
orgGetter: orgGetter,
settings: settings, settings: settings,
servers: make(map[string]*alertmanagerserver.Server), servers: make(map[string]*alertmanagerserver.Server),
serversMtx: sync.RWMutex{}, serversMtx: sync.RWMutex{},
@ -44,38 +56,38 @@ func New(ctx context.Context, settings factory.ScopedProviderSettings, config al
} }
func (service *Service) SyncServers(ctx context.Context) error { func (service *Service) SyncServers(ctx context.Context) error {
orgIDs, err := service.configStore.ListOrgs(ctx) orgs, err := service.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil { if err != nil {
return err return err
} }
service.serversMtx.Lock() service.serversMtx.Lock()
for _, orgID := range orgIDs { for _, org := range orgs {
config, err := service.getConfig(ctx, orgID) config, err := service.getConfig(ctx, org.ID.StringValue())
if err != nil { if err != nil {
service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", "org_id", orgID, "error", err) service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", "org_id", org.ID.StringValue(), "error", err)
continue continue
} }
// If the server is not present, create it and sync the config // If the server is not present, create it and sync the config
if _, ok := service.servers[orgID]; !ok { if _, ok := service.servers[org.ID.StringValue()]; !ok {
server, err := service.newServer(ctx, orgID) server, err := service.newServer(ctx, org.ID.StringValue())
if err != nil { if err != nil {
service.settings.Logger().ErrorContext(ctx, "failed to create alertmanager server", "org_id", orgID, "error", err) service.settings.Logger().ErrorContext(ctx, "failed to create alertmanager server", "org_id", org.ID.StringValue(), "error", err)
continue continue
} }
service.servers[orgID] = server service.servers[org.ID.StringValue()] = server
} }
if service.servers[orgID].Hash() == config.StoreableConfig().Hash { if service.servers[org.ID.StringValue()].Hash() == config.StoreableConfig().Hash {
service.settings.Logger().DebugContext(ctx, "skipping alertmanager sync for org", "org_id", orgID, "hash", config.StoreableConfig().Hash) service.settings.Logger().DebugContext(ctx, "skipping alertmanager sync for org", "org_id", org.ID.StringValue(), "hash", config.StoreableConfig().Hash)
continue continue
} }
err = service.servers[orgID].SetConfig(ctx, config) err = service.servers[org.ID.StringValue()].SetConfig(ctx, config)
if err != nil { if err != nil {
service.settings.Logger().ErrorContext(ctx, "failed to set config for alertmanager server", "org_id", orgID, "error", err) service.settings.Logger().ErrorContext(ctx, "failed to set config for alertmanager server", "org_id", org.ID.StringValue(), "error", err)
continue continue
} }
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore" "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes" "github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
@ -22,13 +23,13 @@ type provider struct {
stopC chan struct{} stopC chan struct{}
} }
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] { func NewFactory(sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) { return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
return New(ctx, settings, config, sqlstore) return New(ctx, settings, config, sqlstore, orgGetter)
}) })
} }
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (*provider, error) { func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore, orgGetter organization.Getter) (*provider, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager") settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager")
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore) configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore) stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
@ -40,6 +41,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
config.Signoz.Config, config.Signoz.Config,
stateStore, stateStore,
configStore, configStore,
orgGetter,
), ),
settings: settings, settings: settings,
config: config, config: config,

View File

@ -5,9 +5,15 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "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/valuer"
)
const (
apiKeyCrossOrgMessage string = "::API-KEY-CROSS-ORG::"
) )
type APIKey struct { type APIKey struct {
@ -15,10 +21,11 @@ type APIKey struct {
uuid *authtypes.UUID uuid *authtypes.UUID
headers []string headers []string
logger *slog.Logger logger *slog.Logger
sharder sharder.Sharder
} }
func NewAPIKey(store sqlstore.SQLStore, headers []string, logger *slog.Logger) *APIKey { func NewAPIKey(store sqlstore.SQLStore, headers []string, logger *slog.Logger, sharder sharder.Sharder) *APIKey {
return &APIKey{store: store, uuid: authtypes.NewUUID(), headers: headers, logger: logger} return &APIKey{store: store, uuid: authtypes.NewUUID(), headers: headers, logger: logger, sharder: sharder}
} }
func (a *APIKey) Wrap(next http.Handler) http.Handler { func (a *APIKey) Wrap(next http.Handler) http.Handler {
@ -36,13 +43,20 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
apiKeyToken, ok := authtypes.UUIDFromContext(ctx) apiKeyToken, ok := authtypes.UUIDFromContext(ctx)
if !ok { if !ok {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
err = a.store.BunDB().NewSelect().Model(&apiKey).Where("token = ?", apiKeyToken).Scan(r.Context()) err = a.
store.
BunDB().
NewSelect().
Model(&apiKey).
Where("token = ?", apiKeyToken).
Scan(r.Context())
if err != nil { if err != nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
@ -71,6 +85,18 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
ctx = authtypes.NewContextWithClaims(ctx, jwt) ctx = authtypes.NewContextWithClaims(ctx, jwt)
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
next.ServeHTTP(w, r)
return
}
if err := a.sharder.IsMyOwnedKey(r.Context(), types.NewOrganizationKey(valuer.MustNewUUID(claims.OrgID))); err != nil {
a.logger.ErrorContext(r.Context(), apiKeyCrossOrgMessage, "claims", claims, "error", err)
next.ServeHTTP(w, r)
return
}
r = r.WithContext(ctx) r = r.WithContext(ctx)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View File

@ -1,18 +1,28 @@
package middleware package middleware
import ( import (
"log/slog"
"net/http" "net/http"
"github.com/SigNoz/signoz/pkg/sharder"
"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/valuer"
)
const (
authCrossOrgMessage string = "::AUTH-CROSS-ORG::"
) )
type Auth struct { type Auth struct {
jwt *authtypes.JWT jwt *authtypes.JWT
headers []string headers []string
sharder sharder.Sharder
logger *slog.Logger
} }
func NewAuth(jwt *authtypes.JWT, headers []string) *Auth { func NewAuth(jwt *authtypes.JWT, headers []string, sharder sharder.Sharder, logger *slog.Logger) *Auth {
return &Auth{jwt: jwt, headers: headers} return &Auth{jwt: jwt, headers: headers, sharder: sharder, logger: logger}
} }
func (a *Auth) Wrap(next http.Handler) http.Handler { func (a *Auth) Wrap(next http.Handler) http.Handler {
@ -28,6 +38,18 @@ func (a *Auth) Wrap(next http.Handler) http.Handler {
return return
} }
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
next.ServeHTTP(w, r)
return
}
if err := a.sharder.IsMyOwnedKey(r.Context(), types.NewOrganizationKey(valuer.MustNewUUID(claims.OrgID))); err != nil {
a.logger.ErrorContext(r.Context(), authCrossOrgMessage, "claims", claims, "error", err)
next.ServeHTTP(w, r)
return
}
r = r.WithContext(ctx) r = r.WithContext(ctx)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View File

@ -4,13 +4,13 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/apdextypes"
) )
type Module interface { type Module interface {
Get(context.Context, string, []string) ([]*types.ApdexSettings, error) Get(context.Context, string, []string) ([]*apdextypes.Settings, error)
Set(context.Context, string, *types.ApdexSettings) error Set(context.Context, string, *apdextypes.Settings) error
} }
type Handler interface { type Handler interface {

View File

@ -9,7 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/apdex" "github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/apdextypes"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
) )
@ -31,7 +31,7 @@ func (handler *handler) Set(rw http.ResponseWriter, req *http.Request) {
return return
} }
var apdexSettings types.ApdexSettings var apdexSettings apdextypes.Settings
if err := json.NewDecoder(req.Body).Decode(&apdexSettings); err != nil { if err := json.NewDecoder(req.Body).Decode(&apdexSettings); err != nil {
render.Error(rw, err) render.Error(rw, err)
return return

View File

@ -6,7 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/apdex" "github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/apdextypes"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun" "github.com/uptrace/bun"
) )
@ -25,8 +25,8 @@ func NewModule(sqlstore sqlstore.SQLStore) apdex.Module {
} }
} }
func (module *module) Get(ctx context.Context, orgID string, services []string) ([]*types.ApdexSettings, error) { func (module *module) Get(ctx context.Context, orgID string, services []string) ([]*apdextypes.Settings, error) {
var apdexSettings []*types.ApdexSettings var apdexSettings []*apdextypes.Settings
err := module. err := module.
sqlstore. sqlstore.
@ -51,7 +51,7 @@ func (module *module) Get(ctx context.Context, orgID string, services []string)
} }
if !found { if !found {
apdexSettings = append(apdexSettings, &types.ApdexSettings{ apdexSettings = append(apdexSettings, &apdextypes.Settings{
ServiceName: service, ServiceName: service,
Threshold: defaultApdexThreshold, Threshold: defaultApdexThreshold,
}) })
@ -61,7 +61,7 @@ func (module *module) Get(ctx context.Context, orgID string, services []string)
return apdexSettings, nil return apdexSettings, nil
} }
func (module *module) Set(ctx context.Context, orgID string, apdexSettings *types.ApdexSettings) error { func (module *module) Set(ctx context.Context, orgID string, apdexSettings *apdextypes.Settings) error {
apdexSettings.OrgID = orgID apdexSettings.OrgID = orgID
apdexSettings.Identifiable.ID = valuer.GenerateUUID() apdexSettings.Identifiable.ID = valuer.GenerateUUID()

View File

@ -4,25 +4,32 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
) )
type Module interface { type Module interface {
Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error) Create(ctx context.Context, orgID valuer.UUID, createdBy string, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error)
List(ctx context.Context, orgID string) ([]*types.Dashboard, error) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error)
Delete(ctx context.Context, orgID, uuid string) error List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
Get(ctx context.Context, orgID, uuid string) (*types.Dashboard, error) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error)
GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error
Update(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}) (*types.Dashboard, error) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
} }
type Handler interface { type Handler interface {
Create(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
LockUnlock(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request) Delete(http.ResponseWriter, *http.Request)
} }

View File

@ -2,12 +2,16 @@ package impldashboard
import ( import (
"context" "context"
"encoding/json"
"net/http" "net/http"
"time" "time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -19,8 +23,8 @@ func NewHandler(module dashboard.Module) dashboard.Handler {
return &handler{module: module} return &handler{module: module}
} }
func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) { func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx) claims, err := authtypes.ClaimsFromContext(ctx)
@ -29,13 +33,151 @@ func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) {
return return
} }
uuid := mux.Vars(req)["uuid"] orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.Delete(ctx, claims.OrgID, uuid) req := dashboardtypes.PostableDashboard{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, gettableDashboard)
}
func (handler *handler) Update(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, err)
return
}
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
req := dashboardtypes.UpdatableDashboard{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.Update(ctx, orgID, dashboardID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, dashboard)
}
func (handler *handler) LockUnlock(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, err)
return
}
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
req := new(dashboardtypes.LockUnlockDashboard)
err = json.NewDecoder(r.Body).Decode(req)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.LockUnlock(ctx, orgID, dashboardID, claims.Email, *req.Locked)
if err != nil { if err != nil {
render.Error(rw, err) render.Error(rw, err)
return return
} }
render.Success(rw, http.StatusOK, nil) render.Success(rw, http.StatusOK, nil)
}
func (handler *handler) Delete(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, err)
return
}
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.Delete(ctx, orgID, dashboardID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
} }

View File

@ -2,164 +2,40 @@ package impldashboard
import ( import (
"context" "context"
"encoding/json"
"strings" "strings"
"time"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/google/uuid" "github.com/SigNoz/signoz/pkg/valuer"
) )
type module struct { type module struct {
sqlstore sqlstore.SQLStore store dashboardtypes.Store
settings factory.ScopedProviderSettings
} }
func NewModule(sqlstore sqlstore.SQLStore) dashboard.Module { func NewModule(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/impldashboard")
return &module{ return &module{
sqlstore: sqlstore, store: NewStore(sqlstore),
settings: scopedProviderSettings,
} }
} }
// CreateDashboard creates a new dashboard func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
func (module *module) Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error) { dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
dash := &types.Dashboard{
Data: data,
}
dash.OrgID = orgID
dash.CreatedAt = time.Now()
dash.CreatedBy = email
dash.UpdatedAt = time.Now()
dash.UpdatedBy = email
dash.UpdateSlug()
dash.UUID = uuid.New().String()
if data["uuid"] != nil {
dash.UUID = data["uuid"].(string)
}
err := module.
sqlstore.
BunDB().
NewInsert().
Model(dash).
Returning("id").
Scan(ctx, &dash.ID)
if err != nil {
return nil, module.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard with uuid %s already exists", dash.UUID)
}
return dash, nil
}
func (module *module) List(ctx context.Context, orgID string) ([]*types.Dashboard, error) {
dashboards := []*types.Dashboard{}
err := module.
sqlstore.
BunDB().
NewSelect().
Model(&dashboards).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return dashboards, nil storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
}
func (module *module) Delete(ctx context.Context, orgID, uuid string) error {
dashboard, err := module.Get(ctx, orgID, uuid)
if err != nil {
return err
}
if dashboard.Locked != nil && *dashboard.Locked == 1 {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be able to delete it")
}
result, err := module.
sqlstore.
BunDB().
NewDelete().
Model(&types.Dashboard{}).
Where("org_id = ?", orgID).
Where("uuid = ?", uuid).
Exec(ctx)
if err != nil {
return err
}
affectedRows, err := result.RowsAffected()
if err != nil {
return err
}
if affectedRows == 0 {
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no dashboard found with uuid: %s", uuid)
}
return nil
}
func (module *module) Get(ctx context.Context, orgID, uuid string) (*types.Dashboard, error) {
dashboard := types.Dashboard{}
err := module.
sqlstore.
BunDB().
NewSelect().
Model(&dashboard).
Where("org_id = ?", orgID).
Where("uuid = ?", uuid).
Scan(ctx)
if err != nil {
return nil, module.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with uuid %s not found", uuid)
}
return &dashboard, nil
}
func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}) (*types.Dashboard, error) {
mapData, err := json.Marshal(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = module.store.Create(ctx, storableDashboard)
dashboard, err := module.Get(ctx, orgID, uuid)
if err != nil {
return nil, err
}
if dashboard.Locked != nil && *dashboard.Locked == 1 {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be able to edit it")
}
// if the total count of panels has reduced by more than 1,
// return error
existingIds := getWidgetIds(dashboard.Data)
newIds := getWidgetIds(data)
differenceIds := getIdDifference(existingIds, newIds)
if len(differenceIds) > 1 {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "deleting more than one panel is not supported")
}
dashboard.UpdatedAt = time.Now()
dashboard.UpdatedBy = userEmail
dashboard.Data = data
_, err = module.sqlstore.
BunDB().
NewUpdate().
Model(dashboard).
Set("updated_at = ?", dashboard.UpdatedAt).
Set("updated_by = ?", userEmail).
Set("data = ?", mapData).
Where("uuid = ?", dashboard.UUID).Exec(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -167,28 +43,73 @@ func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string,
return dashboard, nil return dashboard, nil
} }
func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error { func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
dashboard, err := module.Get(ctx, orgID, uuid) storableDashboard, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
dashboard, err := dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard)
if err != nil {
return nil, err
}
return dashboard, nil
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
storableDashboards, err := module.store.List(ctx, orgID)
if err != nil {
return nil, err
}
dashboards, err := dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards)
if err != nil {
return nil, err
}
return dashboards, nil
}
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error) {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
err = dashboard.Update(updatableDashboard, updatedBy)
if err != nil {
return nil, err
}
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return nil, err
}
err = module.store.Update(ctx, orgID, storableDashboard)
if err != nil {
return nil, err
}
return dashboard, nil
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil { if err != nil {
return err return err
} }
var lockValue int err = dashboard.LockUnlock(ctx, lock, updatedBy)
if lock { if err != nil {
lockValue = 1 return err
} else { }
lockValue = 0 storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return err
} }
_, err = module. err = module.store.Update(ctx, orgID, storableDashboard)
sqlstore.
BunDB().
NewUpdate().
Model(dashboard).
Set("locked = ?", lockValue).
Where("org_id = ?", orgID).
Where("uuid = ?", uuid).
Exec(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -196,15 +117,21 @@ func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock b
return nil return nil
} }
func (module *module) GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error) { func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
dashboards := []types.Dashboard{} dashboard, err := module.Get(ctx, orgID, id)
err := module. if err != nil {
sqlstore. return err
BunDB(). }
NewSelect().
Model(&dashboards). if dashboard.Locked {
Where("org_id = ?", orgID). return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
Scan(ctx) }
return module.store.Delete(ctx, orgID, id)
}
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error) {
dashboards, err := module.List(ctx, orgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -266,7 +193,7 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
for _, metricName := range metricNames { for _, metricName := range metricNames {
if strings.TrimSpace(key) == metricName { if strings.TrimSpace(key) == metricName {
result[metricName] = append(result[metricName], map[string]string{ result[metricName] = append(result[metricName], map[string]string{
"dashboard_id": dashboard.UUID, "dashboard_id": dashboard.ID,
"widget_name": widgetTitle, "widget_name": widgetTitle,
"widget_id": widgetID, "widget_id": widgetID,
"dashboard_name": dashTitle, "dashboard_name": dashTitle,
@ -280,52 +207,3 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
return result, nil return result, nil
} }
func getWidgetIds(data map[string]interface{}) []string {
widgetIds := []string{}
if data != nil && data["widgets"] != nil {
widgets, ok := data["widgets"]
if ok {
data, ok := widgets.([]interface{})
if ok {
for _, widget := range data {
sData, ok := widget.(map[string]interface{})
if ok && sData["query"] != nil && sData["id"] != nil {
id, ok := sData["id"].(string)
if ok {
widgetIds = append(widgetIds, id)
}
}
}
}
}
}
return widgetIds
}
func getIdDifference(existingIds []string, newIds []string) []string {
// Convert newIds array to a map for faster lookups
newIdsMap := make(map[string]bool)
for _, id := range newIds {
newIdsMap[id] = true
}
// Initialize a map to keep track of elements in the difference array
differenceMap := make(map[string]bool)
// Initialize the difference array
difference := []string{}
// Iterate through existingIds
for _, id := range existingIds {
// If the id is not found in newIds, and it's not already in the difference array
if _, found := newIdsMap[id]; !found && !differenceMap[id] {
difference = append(difference, id)
differenceMap[id] = true // Mark the id as seen in the difference array
}
}
return difference
}

View File

@ -0,0 +1,99 @@
package impldashboard
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) dashboardtypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, storabledashboard *dashboardtypes.StorableDashboard) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storabledashboard).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard with id %s already exists", storabledashboard.ID)
}
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.StorableDashboard, error) {
storableDashboard := new(dashboardtypes.StorableDashboard)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableDashboard).
Where("id = ?", id).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
}
return storableDashboard, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.StorableDashboard, error) {
storableDashboards := make([]*dashboardtypes.StorableDashboard, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableDashboards).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no dashboards found in orgID %s", orgID)
}
return storableDashboards, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashboard *dashboardtypes.StorableDashboard) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableDashboard).
WherePK().
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeAlreadyExists, "dashboard with id %s doesn't exist", storableDashboard.ID)
}
return nil
}
func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
NewDelete().
Model(new(dashboardtypes.StorableDashboard)).
Where("id = ?", id).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
}
return nil
}

View File

@ -0,0 +1,36 @@
package implorganization
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type getter struct {
store types.OrganizationStore
sharder sharder.Sharder
}
func NewGetter(store types.OrganizationStore, sharder sharder.Sharder) organization.Getter {
return &getter{store: store, sharder: sharder}
}
func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.Organization, error) {
return module.store.Get(ctx, id)
}
func (module *getter) List(ctx context.Context) ([]*types.Organization, error) {
return module.store.GetAll(ctx)
}
func (module *getter) ListByOwnedKeyRange(ctx context.Context) ([]*types.Organization, error) {
start, end, err := module.sharder.GetMyOwnedKeyRange(ctx)
if err != nil {
return nil, err
}
return module.store.ListByKeyRange(ctx, start, end)
}

View File

@ -15,11 +15,12 @@ import (
) )
type handler struct { type handler struct {
module organization.Module orgGetter organization.Getter
orgSetter organization.Setter
} }
func NewHandler(module organization.Module) organization.Handler { func NewHandler(orgGetter organization.Getter, orgSetter organization.Setter) organization.Handler {
return &handler{module: module} return &handler{orgGetter: orgGetter, orgSetter: orgSetter}
} }
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) { func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
@ -38,7 +39,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
return return
} }
organization, err := handler.module.Get(ctx, orgID) organization, err := handler.orgGetter.Get(ctx, orgID)
if err != nil { if err != nil {
render.Error(rw, err) render.Error(rw, err)
return return
@ -67,10 +68,11 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
err = json.NewDecoder(r.Body).Decode(&req) err = json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
render.Error(rw, err) render.Error(rw, err)
return
} }
req.ID = orgID req.ID = orgID
err = handler.module.Update(ctx, req) err = handler.orgSetter.Update(ctx, req)
if err != nil { if err != nil {
render.Error(rw, err) render.Error(rw, err)
return return

View File

@ -1,33 +0,0 @@
package implorganization
import (
"context"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store types.OrganizationStore
}
func NewModule(organizationStore types.OrganizationStore) organization.Module {
return &module{store: organizationStore}
}
func (module *module) Create(ctx context.Context, organization *types.Organization) error {
return module.store.Create(ctx, organization)
}
func (module *module) Get(ctx context.Context, id valuer.UUID) (*types.Organization, error) {
return module.store.Get(ctx, id)
}
func (module *module) GetAll(ctx context.Context) ([]*types.Organization, error) {
return module.store.GetAll(ctx)
}
func (module *module) Update(ctx context.Context, updatedOrganization *types.Organization) error {
return module.store.Update(ctx, updatedOrganization)
}

View File

@ -0,0 +1,40 @@
package implorganization
import (
"context"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/types"
)
type setter struct {
store types.OrganizationStore
alertmanager alertmanager.Alertmanager
quickfilter quickfilter.Module
}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
}
func (module *setter) Create(ctx context.Context, organization *types.Organization) error {
if err := module.store.Create(ctx, organization); err != nil {
return err
}
if err := module.alertmanager.SetDefaultConfig(ctx, organization.ID.StringValue()); err != nil {
return err
}
if err := module.quickfilter.SetDefaultConfig(ctx, organization.ID); err != nil {
return err
}
return nil
}
func (module *setter) Update(ctx context.Context, updatedOrganization *types.Organization) error {
return module.store.Update(ctx, updatedOrganization)
}

View File

@ -92,3 +92,20 @@ func (store *store) Delete(ctx context.Context, id valuer.UUID) error {
return nil return nil
} }
func (store *store) ListByKeyRange(ctx context.Context, start, end uint32) ([]*types.Organization, error) {
organizations := make([]*types.Organization, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&organizations).
Where("key >= ?", start).
Where("key <= ?", end).
Scan(ctx)
if err != nil {
return nil, err
}
return organizations, nil
}

View File

@ -8,17 +8,22 @@ import (
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
) )
type Module interface { type Getter interface {
// Create creates the given organization
Create(context.Context, *types.Organization) error
// Get gets the organization based on the given id // Get gets the organization based on the given id
Get(context.Context, valuer.UUID) (*types.Organization, error) Get(context.Context, valuer.UUID) (*types.Organization, error)
// GetAll gets all the organizations // Lists all the organizations
GetAll(context.Context) ([]*types.Organization, error) List(context.Context) ([]*types.Organization, error)
// Update updates the given organization based on id present // ListByOwnedKeyRange gets all the organizations owned by the instance
ListByOwnedKeyRange(context.Context) ([]*types.Organization, error)
}
type Setter interface {
// Create creates the given organization
Create(context.Context, *types.Organization) error
// Update updates the given organization
Update(context.Context, *types.Organization) error Update(context.Context, *types.Organization) error
} }

View File

@ -0,0 +1,235 @@
package impltracefunnel
import (
"encoding/json"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/types/authtypes"
tf "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
module tracefunnel.Module
}
func NewHandler(module tracefunnel.Module) tracefunnel.Handler {
return &handler{module: module}
}
func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
var req tf.PostableFunnel
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, valuer.MustNewUUID(claims.UserID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"failed to create funnel: %v", err))
return
}
response := tf.ConstructFunnelResponse(funnel, &claims)
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
var req tf.PostableFunnel
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
if err != nil {
render.Error(rw, err)
return
}
funnel, err := handler.module.Get(r.Context(), req.FunnelID, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"funnel not found: %v", err))
return
}
steps, err := tf.ProcessFunnelSteps(req.Steps)
if err != nil {
render.Error(rw, err)
return
}
funnel.Steps = steps
funnel.UpdatedAt = updatedAt
funnel.UpdatedBy = claims.UserID
if req.Name != "" {
funnel.Name = req.Name
}
if req.Description != "" {
funnel.Description = req.Description
}
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"failed to update funnel in database: %v", err))
return
}
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"failed to get updated funnel: %v", err))
return
}
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
var req tf.PostableFunnel
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, err)
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
if err != nil {
render.Error(rw, err)
return
}
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"funnel not found: %v", err))
return
}
funnel.UpdatedAt = updatedAt
funnel.UpdatedBy = claims.UserID
if req.Name != "" {
funnel.Name = req.Name
}
if req.Description != "" {
funnel.Description = req.Description
}
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"failed to update funnel in database: %v", err))
return
}
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"failed to get updated funnel: %v", err))
return
}
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
funnels, err := handler.module.List(r.Context(), valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"failed to list funnels: %v", err))
return
}
var response []tf.GettableFunnel
for _, f := range funnels {
response = append(response, tf.ConstructFunnelResponse(f, &claims))
}
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"funnel not found: %v", err))
return
}
response := tf.ConstructFunnelResponse(funnel, &claims)
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(rw, err)
return
}
if err := handler.module.Delete(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID)); err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
errors.CodeInvalidInput,
"failed to delete funnel: %v", err))
return
}
render.Success(rw, http.StatusOK, nil)
}

View File

@ -0,0 +1,173 @@
package impltracefunnel
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockModule struct {
mock.Mock
}
func (m *MockModule) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, timestamp, name, userID, orgID)
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockModule) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, funnelID, orgID)
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockModule) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
args := m.Called(ctx, funnel, userID)
return args.Error(0)
}
func (m *MockModule) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockModule) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
args := m.Called(ctx, funnelID, orgID)
return args.Error(0)
}
func (m *MockModule) Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID, orgID valuer.UUID) error {
args := m.Called(ctx, funnel, userID, orgID)
return args.Error(0)
}
func (m *MockModule) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
args := m.Called(ctx, funnelID, orgID)
return args.Get(0).(int64), args.Get(1).(int64), args.String(2), args.Error(3)
}
func TestHandler_List(t *testing.T) {
mockModule := new(MockModule)
handler := NewHandler(mockModule)
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/list", nil)
orgID := valuer.GenerateUUID()
claims := authtypes.Claims{
OrgID: orgID.String(),
}
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
rr := httptest.NewRecorder()
funnel1ID := valuer.GenerateUUID()
funnel2ID := valuer.GenerateUUID()
expectedFunnels := []*traceFunnels.StorableFunnel{
{
Identifiable: types.Identifiable{
ID: funnel1ID,
},
Name: "funnel-1",
OrgID: orgID,
},
{
Identifiable: types.Identifiable{
ID: funnel2ID,
},
Name: "funnel-2",
OrgID: orgID,
},
}
mockModule.On("List", req.Context(), orgID).Return(expectedFunnels, nil)
handler.List(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response struct {
Status string `json:"status"`
Data []traceFunnels.GettableFunnel `json:"data"`
}
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "success", response.Status)
assert.Len(t, response.Data, 2)
assert.Equal(t, "funnel-1", response.Data[0].FunnelName)
assert.Equal(t, "funnel-2", response.Data[1].FunnelName)
mockModule.AssertExpectations(t)
}
func TestHandler_Get(t *testing.T) {
mockModule := new(MockModule)
handler := NewHandler(mockModule)
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/"+funnelID.String(), nil)
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
OrgID: orgID.String(),
}))
rr := httptest.NewRecorder()
expectedFunnel := &traceFunnels.StorableFunnel{
Identifiable: types.Identifiable{
ID: funnelID,
},
Name: "test-funnel",
OrgID: orgID,
}
mockModule.On("Get", req.Context(), funnelID, orgID).Return(expectedFunnel, nil)
handler.Get(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response struct {
Status string `json:"status"`
Data traceFunnels.GettableFunnel `json:"data"`
}
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "success", response.Status)
assert.Equal(t, "test-funnel", response.Data.FunnelName)
assert.Equal(t, expectedFunnel.OrgID.String(), response.Data.OrgID)
mockModule.AssertExpectations(t)
}
func TestHandler_Delete(t *testing.T) {
mockModule := new(MockModule)
handler := NewHandler(mockModule)
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/trace-funnels/"+funnelID.String(), nil)
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
OrgID: orgID.String(),
}))
rr := httptest.NewRecorder()
mockModule.On("Delete", req.Context(), funnelID, orgID).Return(nil)
handler.Delete(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
mockModule.AssertExpectations(t)
}

View File

@ -0,0 +1,96 @@
package impltracefunnel
import (
"context"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/types"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store traceFunnels.FunnelStore
}
func NewModule(store traceFunnels.FunnelStore) tracefunnel.Module {
return &module{
store: store,
}
}
func (module *module) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
funnel := &traceFunnels.StorableFunnel{
Name: name,
OrgID: orgID,
}
funnel.CreatedAt = time.Unix(0, timestamp*1000000) // Convert to nanoseconds
funnel.CreatedBy = userID.String()
// Set up the user relationship
funnel.CreatedByUser = &types.User{
Identifiable: types.Identifiable{
ID: userID,
},
}
if funnel.ID.IsZero() {
funnel.ID = valuer.GenerateUUID()
}
if funnel.CreatedAt.IsZero() {
funnel.CreatedAt = time.Now()
}
if funnel.UpdatedAt.IsZero() {
funnel.UpdatedAt = time.Now()
}
// Set created_by if CreatedByUser is present
if funnel.CreatedByUser != nil {
funnel.CreatedBy = funnel.CreatedByUser.Identifiable.ID.String()
}
if err := module.store.Create(ctx, funnel); err != nil {
return nil, fmt.Errorf("failed to create funnel: %v", err)
}
return funnel, nil
}
// Get gets a funnel by ID
func (module *module) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
return module.store.Get(ctx, funnelID, orgID)
}
// Update updates a funnel
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
funnel.UpdatedBy = userID.String()
return module.store.Update(ctx, funnel)
}
// List lists all funnels for an organization
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
funnels, err := module.store.List(ctx, orgID)
if err != nil {
return nil, fmt.Errorf("failed to list funnels: %v", err)
}
return funnels, nil
}
// Delete deletes a funnel
func (module *module) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
return module.store.Delete(ctx, funnelID, orgID)
}
// GetFunnelMetadata gets metadata for a funnel
func (module *module) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
funnel, err := module.store.Get(ctx, funnelID, orgID)
if err != nil {
return 0, 0, "", err
}
return funnel.CreatedAt.UnixNano() / 1000000, funnel.UpdatedAt.UnixNano() / 1000000, funnel.Description, nil
}

View File

@ -0,0 +1,114 @@
package impltracefunnel
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) traceFunnels.FunnelStore {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
// Check if a funnel with the same name already exists in the organization
exists, err := store.
sqlstore.
BunDB().
NewSelect().
Model(new(traceFunnels.StorableFunnel)).
Where("name = ? AND org_id = ?", funnel.Name, funnel.OrgID.String()).
Exists(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to check for existing funnelr")
}
if exists {
return store.sqlstore.WrapAlreadyExistsErrf(nil, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)
}
_, err = store.
sqlstore.
BunDB().
NewInsert().
Model(funnel).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create funnels")
}
return nil
}
// Get retrieves a funnel by ID
func (store *store) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
funnel := &traceFunnels.StorableFunnel{}
err := store.
sqlstore.
BunDB().
NewSelect().
Model(funnel).
Relation("CreatedByUser").
Where("?TableAlias.id = ? AND ?TableAlias.org_id = ?", uuid.String(), orgID.String()).
Scan(ctx)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get funnels")
}
return funnel, nil
}
// Update updates an existing funnel
func (store *store) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
funnel.UpdatedAt = time.Now()
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(funnel).
WherePK().
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)
}
return nil
}
// List retrieves all funnels for a given organization
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
var funnels []*traceFunnels.StorableFunnel
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&funnels).
Relation("CreatedByUser").
Where("?TableAlias.org_id = ?", orgID.String()).
Scan(ctx)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to list funnels")
}
return funnels, nil
}
// Delete removes a funnel by ID
func (store *store) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
NewDelete().
Model(new(traceFunnels.StorableFunnel)).
Where("id = ? AND org_id = ?", funnelID.String(), orgID.String()).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete funnel")
}
return nil
}

View File

@ -0,0 +1,38 @@
package tracefunnel
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
"net/http"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
)
// Module defines the interface for trace funnel operations
type Module interface {
Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error
List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error)
Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error
GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error)
}
type Handler interface {
New(http.ResponseWriter, *http.Request)
UpdateSteps(http.ResponseWriter, *http.Request)
UpdateFunnel(http.ResponseWriter, *http.Request)
List(http.ResponseWriter, *http.Request)
Get(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
}

View File

@ -0,0 +1,183 @@
package tracefunneltest
import (
"context"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
"github.com/SigNoz/signoz/pkg/types"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockStore struct {
mock.Mock
}
func (m *MockStore) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
args := m.Called(ctx, funnel)
return args.Error(0)
}
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, uuid, orgID)
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockStore) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
args := m.Called(ctx, orgID)
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
}
func (m *MockStore) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
args := m.Called(ctx, funnel)
return args.Error(0)
}
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) error {
args := m.Called(ctx, uuid, orgID)
return args.Error(0)
}
func TestModule_Create(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
timestamp := time.Now().UnixMilli()
name := "test-funnel"
userID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
return f.Name == name &&
f.CreatedBy == userID.String() &&
f.OrgID == orgID &&
f.CreatedByUser != nil &&
f.CreatedByUser.ID == userID &&
f.CreatedAt.UnixNano()/1000000 == timestamp
})).Return(nil)
funnel, err := module.Create(ctx, timestamp, name, userID, orgID)
assert.NoError(t, err)
assert.NotNil(t, funnel)
assert.Equal(t, name, funnel.Name)
assert.Equal(t, userID.String(), funnel.CreatedBy)
assert.Equal(t, orgID, funnel.OrgID)
assert.NotNil(t, funnel.CreatedByUser)
assert.Equal(t, userID, funnel.CreatedByUser.ID)
mockStore.AssertExpectations(t)
}
func TestModule_Get(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
expectedFunnel := &traceFunnels.StorableFunnel{
Name: "test-funnel",
}
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
funnel, err := module.Get(ctx, funnelID, orgID)
assert.NoError(t, err)
assert.Equal(t, expectedFunnel, funnel)
mockStore.AssertExpectations(t)
}
func TestModule_Update(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
userID := valuer.GenerateUUID()
funnel := &traceFunnels.StorableFunnel{
Name: "test-funnel",
}
mockStore.On("Update", ctx, funnel).Return(nil)
err := module.Update(ctx, funnel, userID)
assert.NoError(t, err)
assert.Equal(t, userID.String(), funnel.UpdatedBy)
mockStore.AssertExpectations(t)
}
func TestModule_List(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
orgID := valuer.GenerateUUID()
expectedFunnels := []*traceFunnels.StorableFunnel{
{
Name: "funnel-1",
OrgID: orgID,
},
{
Name: "funnel-2",
OrgID: orgID,
},
}
mockStore.On("List", ctx, orgID).Return(expectedFunnels, nil)
funnels, err := module.List(ctx, orgID)
assert.NoError(t, err)
assert.Len(t, funnels, 2)
assert.Equal(t, expectedFunnels, funnels)
mockStore.AssertExpectations(t)
}
func TestModule_Delete(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
mockStore.On("Delete", ctx, funnelID, orgID).Return(nil)
err := module.Delete(ctx, funnelID, orgID)
assert.NoError(t, err)
mockStore.AssertExpectations(t)
}
func TestModule_GetFunnelMetadata(t *testing.T) {
mockStore := new(MockStore)
module := impltracefunnel.NewModule(mockStore)
ctx := context.Background()
funnelID := valuer.GenerateUUID()
orgID := valuer.GenerateUUID()
now := time.Now()
expectedFunnel := &traceFunnels.StorableFunnel{
Description: "test description",
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
}
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
createdAt, updatedAt, description, err := module.GetFunnelMetadata(ctx, funnelID, orgID)
assert.NoError(t, err)
assert.Equal(t, now.UnixNano()/1000000, createdAt)
assert.Equal(t, now.UnixNano()/1000000, updatedAt)
assert.Equal(t, "test description", description)
mockStore.AssertExpectations(t)
}

View File

@ -11,8 +11,10 @@ import (
"github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/query-service/constants" "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/telemetry" "github.com/SigNoz/signoz/pkg/query-service/telemetry"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
@ -22,20 +24,22 @@ import (
) )
type Module struct { type Module struct {
store types.UserStore store types.UserStore
jwt *authtypes.JWT jwt *authtypes.JWT
emailing emailing.Emailing emailing emailing.Emailing
settings factory.ScopedProviderSettings settings factory.ScopedProviderSettings
orgSetter organization.Setter
} }
// This module is a WIP, don't take inspiration from this. // This module is a WIP, don't take inspiration from this.
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module { func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter) user.Module {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser") settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
return &Module{ return &Module{
store: store, store: store,
jwt: jwt, jwt: jwt,
emailing: emailing, emailing: emailing,
settings: settings, settings: settings,
orgSetter: orgSetter,
} }
} }
@ -538,3 +542,36 @@ func (m *Module) ListDomains(ctx context.Context, orgID valuer.UUID) ([]*types.G
func (m *Module) UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) error { func (m *Module) UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) error {
return m.store.UpdateDomain(ctx, domain) return m.store.UpdateDomain(ctx, domain)
} }
func (m *Module) Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin) (*types.User, error) {
if req.Email == "" {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "email is required")
}
if req.Password == "" {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password is required")
}
organization := types.NewOrganization(req.OrgDisplayName)
err := m.orgSetter.Create(ctx, organization)
if err != nil {
return nil, model.InternalError(err)
}
user, err := types.NewUser(req.Name, req.Email, types.RoleAdmin.String(), organization.ID.StringValue())
if err != nil {
return nil, model.InternalError(err)
}
password, err := types.NewFactorPassword(req.Password)
if err != nil {
return nil, model.InternalError(err)
}
user, err = m.CreateUserWithPassword(ctx, user, password)
if err != nil {
return nil, model.InternalError(err)
}
return user, nil
}

View File

@ -62,6 +62,9 @@ type Module interface {
ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error)
RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error
GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error) GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error)
// Register
Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin) (*types.User, error)
} }
type Handler interface { type Handler interface {

View File

@ -4,11 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/SigNoz/signoz/pkg/query-service/constants" "github.com/SigNoz/signoz/pkg/query-service/constants"
"math"
"strconv" "strconv"
"strings" "strings"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/telemetrystore"
promValue "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/prompb" "github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote" "github.com/prometheus/prometheus/storage/remote"
@ -188,9 +190,10 @@ func (client *client) querySamples(ctx context.Context, start int64, end int64,
var fingerprint, prevFingerprint uint64 var fingerprint, prevFingerprint uint64
var timestampMs int64 var timestampMs int64
var value float64 var value float64
var flags uint32
for rows.Next() { for rows.Next() {
if err := rows.Scan(&metricName, &fingerprint, &timestampMs, &value); err != nil { if err := rows.Scan(&metricName, &fingerprint, &timestampMs, &value, &flags); err != nil {
return nil, err return nil, err
} }
@ -208,6 +211,10 @@ func (client *client) querySamples(ctx context.Context, start int64, end int64,
} }
} }
if flags&1 == 1 {
value = math.Float64frombits(promValue.StaleNaN)
}
// add samples to current time series // add samples to current time series
ts.Samples = append(ts.Samples, prompb.Sample{ ts.Samples = append(ts.Samples, prompb.Sample{
Timestamp: timestampMs, Timestamp: timestampMs,

View File

@ -6267,9 +6267,6 @@ func (r *ClickHouseReader) GetUpdatedMetricsMetadata(ctx context.Context, orgID
if err == nil { if err == nil {
cachedMetadata[metricName] = metadata cachedMetadata[metricName] = metadata
} else { } else {
if err != nil {
zap.L().Error("Error retrieving metrics metadata from cache", zap.String("metric_name", metricName), zap.Error(err))
}
missingMetrics = append(missingMetrics, metricName) missingMetrics = append(missingMetrics, metricName)
} }
} }

View File

@ -13,6 +13,8 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
@ -32,9 +34,7 @@ type Controller struct {
serviceConfigRepo ServiceConfigDatabase serviceConfigRepo ServiceConfigDatabase
} }
func NewController(sqlStore sqlstore.SQLStore) ( func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
*Controller, error,
) {
accountsRepo, err := newCloudProviderAccountsRepository(sqlStore) accountsRepo, err := newCloudProviderAccountsRepository(sqlStore)
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err) return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
@ -55,9 +55,7 @@ type ConnectedAccountsListResponse struct {
Accounts []types.Account `json:"accounts"` Accounts []types.Account `json:"accounts"`
} }
func (c *Controller) ListConnectedAccounts( func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) (
ctx context.Context, orgId string, cloudProvider string,
) (
*ConnectedAccountsListResponse, *model.ApiError, *ConnectedAccountsListResponse, *model.ApiError,
) { ) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
@ -103,9 +101,7 @@ type GenerateConnectionUrlResponse struct {
ConnectionUrl string `json:"connection_url"` ConnectionUrl string `json:"connection_url"`
} }
func (c *Controller) GenerateConnectionUrl( func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest) (*GenerateConnectionUrlResponse, *model.ApiError) {
ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest,
) (*GenerateConnectionUrlResponse, *model.ApiError) {
// Account connection with a simple connection URL may not be available for all providers. // Account connection with a simple connection URL may not be available for all providers.
if cloudProvider != "aws" { if cloudProvider != "aws" {
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider)) return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
@ -154,9 +150,7 @@ type AccountStatusResponse struct {
Status types.AccountStatus `json:"status"` Status types.AccountStatus `json:"status"`
} }
func (c *Controller) GetAccountStatus( func (c *Controller) GetAccountStatus(ctx context.Context, orgId string, cloudProvider string, accountId string) (
ctx context.Context, orgId string, cloudProvider string, accountId string,
) (
*AccountStatusResponse, *model.ApiError, *AccountStatusResponse, *model.ApiError,
) { ) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
@ -198,9 +192,7 @@ type IntegrationConfigForAgent struct {
TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"` TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"`
} }
func (c *Controller) CheckInAsAgent( func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest) (*AgentCheckInResponse, error) {
ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest,
) (*AgentCheckInResponse, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -293,13 +285,7 @@ type UpdateAccountConfigRequest struct {
Config types.AccountConfig `json:"config"` Config types.AccountConfig `json:"config"`
} }
func (c *Controller) UpdateAccountConfig( func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*types.Account, *model.ApiError) {
ctx context.Context,
orgId string,
cloudProvider string,
accountId string,
req UpdateAccountConfigRequest,
) (*types.Account, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -316,9 +302,7 @@ func (c *Controller) UpdateAccountConfig(
return &account, nil return &account, nil
} }
func (c *Controller) DisconnectAccount( func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*types.CloudIntegration, *model.ApiError) {
ctx context.Context, orgId string, cloudProvider string, accountId string,
) (*types.CloudIntegration, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -520,10 +504,8 @@ func (c *Controller) UpdateServiceConfig(
// All dashboards that are available based on cloud integrations configuration // All dashboards that are available based on cloud integrations configuration
// across all cloud providers // across all cloud providers
func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) ( func (c *Controller) AvailableDashboards(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) {
[]*types.Dashboard, *model.ApiError, allDashboards := []*dashboardtypes.Dashboard{}
) {
allDashboards := []*types.Dashboard{}
for _, provider := range []string{"aws"} { for _, provider := range []string{"aws"} {
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider) providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider)
@ -539,11 +521,8 @@ func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) (
return allDashboards, nil return allDashboards, nil
} }
func (c *Controller) AvailableDashboardsForCloudProvider( func (c *Controller) AvailableDashboardsForCloudProvider(ctx context.Context, orgID valuer.UUID, cloudProvider string) ([]*dashboardtypes.Dashboard, *model.ApiError) {
ctx context.Context, orgID string, cloudProvider string, accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID.StringValue(), cloudProvider)
) ([]*types.Dashboard, *model.ApiError) {
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID, cloudProvider)
if apiErr != nil { if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts") return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
} }
@ -554,7 +533,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
for _, ar := range accountRecords { for _, ar := range accountRecords {
if ar.AccountID != nil { if ar.AccountID != nil {
configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount( configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount(
ctx, orgID, ar.ID.StringValue(), ctx, orgID.StringValue(), ar.ID.StringValue(),
) )
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
@ -573,16 +552,15 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
return nil, apiErr return nil, apiErr
} }
svcDashboards := []*types.Dashboard{} svcDashboards := []*dashboardtypes.Dashboard{}
for _, svc := range allServices { for _, svc := range allServices {
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id] serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
if serviceDashboardsCreatedAt != nil { if serviceDashboardsCreatedAt != nil {
for _, d := range svc.Assets.Dashboards { for _, d := range svc.Assets.Dashboards {
isLocked := 1
author := fmt.Sprintf("%s-integration", cloudProvider) author := fmt.Sprintf("%s-integration", cloudProvider)
svcDashboards = append(svcDashboards, &types.Dashboard{ svcDashboards = append(svcDashboards, &dashboardtypes.Dashboard{
UUID: c.dashboardUuid(cloudProvider, svc.Id, d.Id), ID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
Locked: &isLocked, Locked: true,
Data: *d.Definition, Data: *d.Definition,
TimeAuditable: types.TimeAuditable{ TimeAuditable: types.TimeAuditable{
CreatedAt: *serviceDashboardsCreatedAt, CreatedAt: *serviceDashboardsCreatedAt,
@ -592,6 +570,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
CreatedBy: author, CreatedBy: author,
UpdatedBy: author, UpdatedBy: author,
}, },
OrgID: orgID,
}) })
} }
servicesWithAvailableMetrics[svc.Id] = nil servicesWithAvailableMetrics[svc.Id] = nil
@ -600,11 +579,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
return svcDashboards, nil return svcDashboards, nil
} }
func (c *Controller) GetDashboardById( func (c *Controller) GetDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
ctx context.Context,
orgId string,
dashboardUuid string,
) (*types.Dashboard, *model.ApiError) {
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid) cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
@ -612,38 +587,28 @@ func (c *Controller) GetDashboardById(
allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider) allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider)
if apiErr != nil { if apiErr != nil {
return nil, model.WrapApiError( return nil, model.WrapApiError(apiErr, "couldn't list available dashboards")
apiErr, fmt.Sprintf("couldn't list available dashboards"),
)
} }
for _, d := range allDashboards { for _, d := range allDashboards {
if d.UUID == dashboardUuid { if d.ID == dashboardUuid {
return d, nil return d, nil
} }
} }
return nil, model.NotFoundError(fmt.Errorf( return nil, model.NotFoundError(fmt.Errorf("couldn't find dashboard with uuid: %s", dashboardUuid))
"couldn't find dashboard with uuid: %s", dashboardUuid,
))
} }
func (c *Controller) dashboardUuid( func (c *Controller) dashboardUuid(
cloudProvider string, svcId string, dashboardId string, cloudProvider string, svcId string, dashboardId string,
) string { ) string {
return fmt.Sprintf( return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
"cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId,
)
} }
func (c *Controller) parseDashboardUuid(dashboardUuid string) ( func (c *Controller) parseDashboardUuid(dashboardUuid string) (cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError) {
cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError,
) {
parts := strings.SplitN(dashboardUuid, "--", 4) parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 || parts[0] != "cloud-integration" { if len(parts) != 4 || parts[0] != "cloud-integration" {
return "", "", "", model.BadRequest(fmt.Errorf( return "", "", "", model.BadRequest(fmt.Errorf("invalid cloud integration dashboard id"))
"invalid cloud integration dashboard id",
))
} }
return parts[1], parts[2], parts[3], nil return parts[1], parts[2], parts[3], nil

View File

@ -3,17 +3,23 @@ package cloudintegrations
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/emailing/noopemailing" "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization" "github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
"github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/utils" "github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -24,11 +30,16 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
controller, err := NewController(sqlStore) controller, err := NewController(sqlStore)
require.NoError(err) require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings() providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{}) sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings) require.NoError(err)
user, apiErr := createTestUser(organizationModule, userModule) orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
require.NoError(err)
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
emailing := emailingtest.New()
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
require.Nil(apiErr) require.Nil(apiErr)
// should be able to generate connection url for // should be able to generate connection url for
@ -74,11 +85,17 @@ func TestAgentCheckIns(t *testing.T) {
sqlStore := utils.NewQueryServiceDBForTests(t) sqlStore := utils.NewQueryServiceDBForTests(t)
controller, err := NewController(sqlStore) controller, err := NewController(sqlStore)
require.NoError(err) require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings() providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{}) sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings) require.NoError(err)
user, apiErr := createTestUser(organizationModule, userModule) orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
require.NoError(err)
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
emailing := emailingtest.New()
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
require.Nil(apiErr) require.Nil(apiErr)
// An agent should be able to check in from a cloud account even // An agent should be able to check in from a cloud account even
@ -164,11 +181,16 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
controller, err := NewController(sqlStore) controller, err := NewController(sqlStore)
require.NoError(err) require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings() providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{}) sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings) require.NoError(err)
user, apiErr := createTestUser(organizationModule, userModule) orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
require.NoError(err)
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
emailing := emailingtest.New()
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
require.Nil(apiErr) require.Nil(apiErr)
// Attempting to disconnect a non-existent account should return error // Attempting to disconnect a non-existent account should return error
@ -186,11 +208,16 @@ func TestConfigureService(t *testing.T) {
controller, err := NewController(sqlStore) controller, err := NewController(sqlStore)
require.NoError(err) require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings() providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{}) sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings) require.NoError(err)
user, apiErr := createTestUser(organizationModule, userModule) orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
require.NoError(err)
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
emailing := emailingtest.New()
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
require.Nil(apiErr) require.Nil(apiErr)
// create a connected account // create a connected account
@ -305,7 +332,7 @@ func makeTestConnectedAccount(t *testing.T, orgId string, controller *Controller
return acc return acc
} }
func createTestUser(organizationModule organization.Module, userModule user.Module) (*types.User, *model.ApiError) { func createTestUser(organizationModule organization.Setter, userModule user.Module) (*types.User, *model.ApiError) {
// Create a test user for auth // Create a test user for auth
ctx := context.Background() ctx := context.Background()
organization := types.NewOrganization("test") organization := types.NewOrganization("test")

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