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
vendor/
**/main/**
__debug_bin**
# git-town
.git-branches.toml

View File

@ -207,3 +207,11 @@ emailing:
key_file_path:
# The path to the certificate file.
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 (
"context"
"encoding/json"
"github.com/SigNoz/signoz/ee/query-service/constants"
"time"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
@ -19,23 +21,31 @@ import (
)
type provider struct {
store licensetypes.Store
zeus zeus.Zeus
config licensing.Config
settings factory.ScopedProviderSettings
stopChan chan struct{}
store licensetypes.Store
zeus zeus.Zeus
config licensing.Config
settings factory.ScopedProviderSettings
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 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")
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 {
@ -67,13 +77,13 @@ func (provider *provider) Stop(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 {
return err
}
for _, organizationID := range organizations {
err := provider.Refresh(ctx, organizationID)
for _, organization := range organizations {
err := provider.Refresh(ctx, organization.ID)
if err != nil {
return err
}
@ -168,6 +178,11 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
err = provider.InitFeatures(ctx, activeLicense.Features)
if err != nil {
return err
}
return nil
}

View File

@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
@ -82,31 +81,6 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
return nil
}
func (store *store) ListOrganizations(ctx context.Context) ([]valuer.UUID, error) {
orgIDStrs := make([]string, 0)
err := store.sqlstore.
BunDB().
NewSelect().
Model(new(types.Organization)).
Column("id").
Scan(ctx, &orgIDStrs)
if err != nil {
return nil, err
}
orgIDs := make([]valuer.UUID, len(orgIDStrs))
for idx, orgIDStr := range orgIDStrs {
orgID, err := valuer.NewUUID(orgIDStr)
if err != nil {
return nil, err
}
orgIDs[idx] = orgID
}
return orgIDs, nil
}
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.

View File

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

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

View File

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

View File

@ -41,16 +41,16 @@ type Manager struct {
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{
clickhouseConn: clickhouseConn,
licenseService: licenseService,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
zeus: zeus,
organizationModule: organizationModule,
clickhouseConn: clickhouseConn,
licenseService: licenseService,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
zeus: zeus,
orgGetter: orgGetter,
}
return m, nil
}
@ -74,8 +74,7 @@ func (lm *Manager) Start(ctx context.Context) error {
return nil
}
func (lm *Manager) UploadUsage(ctx context.Context) {
organizations, err := lm.organizationModule.GetAll(context.Background())
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
zap.L().Error("failed to get organizations", zap.Error(err))
return

View File

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

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

View File

@ -7,7 +7,7 @@ import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
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 dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@ -58,7 +58,7 @@ function LogGeneralField({
const html = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(fieldValue), {
dompurify.sanitize(unescapeString(escapeHtml(fieldValue)), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -259,6 +259,24 @@ export const getDataTypes = (value: unknown): DataTypes => {
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
// the log line readable
export const removeEscapeCharacters = (str: string): string =>

View File

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

View File

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

View File

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

View File

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

View File

@ -171,7 +171,8 @@ function VariablesSetting({
updateMutation.mutateAsync(
{
...selectedDashboard,
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
variables: updatedVariablesData,
@ -179,18 +180,13 @@ function VariablesSetting({
},
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
notifications.success({
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,
queryType: currentQuery.queryType,
widgetId: selectedWidget.id,
dashboardId: selectedDashboard?.uuid,
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
isNewPanel,
});

View File

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

View File

@ -26,6 +26,7 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
@ -301,10 +302,11 @@ function SideNav(): JSX.Element {
}
const isOnBasicPlan =
activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
);
(activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
)) ||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) {
updatedMenuItems = updatedMenuItems.filter(
@ -353,6 +355,7 @@ function SideNav(): JSX.Element {
t,
user.role,
activeLicenseFetchError,
activeLicense?.status,
]);
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 { useErrorModal } from 'providers/ErrorModalProvider';
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 = (
id: string,
): UseMutationResult<PayloadProps, unknown, void, unknown> =>
useMutation({
): UseMutationResult<SuccessResponseV2<null>, APIError, void, unknown> => {
const { showErrorModal } = useErrorModal();
return useMutation<SuccessResponseV2<null>, APIError>({
mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD,
mutationFn: () =>
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 { useErrorModal } from 'providers/ErrorModalProvider';
import { useQuery, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
export const useGetAllDashboard = (): UseQueryResult<Dashboard[], unknown> =>
useQuery<Dashboard[]>({
queryFn: getAllDashboardList,
export const useGetAllDashboard = (): UseQueryResult<
SuccessResponseV2<Dashboard[]>,
APIError
> => {
const { showErrorModal } = useErrorModal();
return useQuery<SuccessResponseV2<Dashboard[]>, APIError>({
queryFn: getAll,
onError: (error) => {
showErrorModal(error);
},
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 { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
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 { Props } from 'types/api/dashboard/update';
import APIError from 'types/api/error';
export const useUpdateDashboard = (): UseUpdateDashboard => {
const { updatedTimeRef } = useDashboard();
const { showErrorModal } = useErrorModal();
return useMutation(update, {
onSuccess: (data) => {
if (data.payload) {
updatedTimeRef.current = dayjs(data.payload.updatedAt);
if (data.data) {
updatedTimeRef.current = dayjs(data.data.updatedAt);
}
},
onError: (error) => {
showErrorModal(error);
},
});
};
type UseUpdateDashboard = UseMutationResult<
SuccessResponse<Dashboard> | ErrorResponse,
unknown,
SuccessResponseV2<Dashboard>,
APIError,
Props,
unknown
>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,10 @@
import { Dashboard } from './getAll';
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 { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse';
export type PayloadProps = Dashboard[];
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
export type TVariableQueryType = typeof VariableQueryTypeArr[number];
@ -50,14 +48,18 @@ export interface IDashboardVariable {
change?: boolean;
}
export interface Dashboard {
id: number;
uuid: string;
id: string;
createdAt: string;
updatedAt: string;
createdBy: string;
updatedBy: string;
data: DashboardData;
isLocked?: boolean;
locked?: boolean;
}
export interface PayloadProps {
data: Dashboard[];
status: string;
}
export interface DashboardTemplate {
@ -69,7 +71,7 @@ export interface DashboardTemplate {
}
export interface DashboardData {
uuid?: string;
// uuid?: string;
description?: string;
tags?: 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';
export type Props = {
uuid: Dashboard['uuid'];
id: Dashboard['id'];
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 {
SUSPENDED = 'SUSPENDED',
VALID = 'VALID',
INVALID = 'INVALID',
}
export enum LicenseState {

2
go.mod
View File

@ -26,7 +26,6 @@ require (
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0
github.com/gosimple/slug v1.10.0
github.com/huandu/go-sqlbuilder v1.35.0
github.com/jackc/pgx/v5 v5.7.2
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/gax-go/v2 v2.14.0 // 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/grpc-ecosystem/grpc-gateway/v2 v2.24.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/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
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/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
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...)
}
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 {
return store.wrap(ctx, func(ctx context.Context) error {
if _, err := store.

View File

@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerbatcher"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
@ -57,16 +58,17 @@ type provider struct {
configStore alertmanagertypes.ConfigStore
batcher *alertmanagerbatcher.Batcher
url *url.URL
orgGetter organization.Getter
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 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")
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.
// Since this is the legacy alertmanager, we get the first org from the store.
if provider.orgID == "" {
orgIDs, err := provider.configStore.ListOrgs(ctx)
orgIDs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", err)
continue
@ -103,7 +105,7 @@ func (provider *provider) Start(ctx context.Context) error {
continue
}
provider.orgID = orgIDs[0]
provider.orgID = orgIDs[0].ID.String()
}
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/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
)
@ -20,6 +21,9 @@ type Service struct {
// configStore is the config store for the alertmanager service
configStore alertmanagertypes.ConfigStore
// organization is the organization module for the alertmanager service
orgGetter organization.Getter
// settings is the settings for the alertmanager service
settings factory.ScopedProviderSettings
@ -30,11 +34,19 @@ type Service struct {
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{
config: config,
stateStore: stateStore,
configStore: configStore,
orgGetter: orgGetter,
settings: settings,
servers: make(map[string]*alertmanagerserver.Server),
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 {
orgIDs, err := service.configStore.ListOrgs(ctx)
orgs, err := service.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
return err
}
service.serversMtx.Lock()
for _, orgID := range orgIDs {
config, err := service.getConfig(ctx, orgID)
for _, org := range orgs {
config, err := service.getConfig(ctx, org.ID.StringValue())
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
}
// If the server is not present, create it and sync the config
if _, ok := service.servers[orgID]; !ok {
server, err := service.newServer(ctx, orgID)
if _, ok := service.servers[org.ID.StringValue()]; !ok {
server, err := service.newServer(ctx, org.ID.StringValue())
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
}
service.servers[orgID] = server
service.servers[org.ID.StringValue()] = server
}
if service.servers[orgID].Hash() == config.StoreableConfig().Hash {
service.settings.Logger().DebugContext(ctx, "skipping alertmanager sync for org", "org_id", 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", org.ID.StringValue(), "hash", config.StoreableConfig().Hash)
continue
}
err = service.servers[orgID].SetConfig(ctx, config)
err = service.servers[org.ID.StringValue()].SetConfig(ctx, config)
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
}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
@ -22,13 +23,13 @@ type provider 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 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")
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
@ -40,6 +41,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
config.Signoz.Config,
stateStore,
configStore,
orgGetter,
),
settings: settings,
config: config,

View File

@ -5,9 +5,15 @@ import (
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
const (
apiKeyCrossOrgMessage string = "::API-KEY-CROSS-ORG::"
)
type APIKey struct {
@ -15,10 +21,11 @@ type APIKey struct {
uuid *authtypes.UUID
headers []string
logger *slog.Logger
sharder sharder.Sharder
}
func NewAPIKey(store sqlstore.SQLStore, headers []string, logger *slog.Logger) *APIKey {
return &APIKey{store: store, uuid: authtypes.NewUUID(), headers: headers, logger: logger}
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, sharder: sharder}
}
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)
return
}
apiKeyToken, ok := authtypes.UUIDFromContext(ctx)
if !ok {
next.ServeHTTP(w, r)
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 {
next.ServeHTTP(w, r)
return
@ -71,6 +85,18 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
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)
next.ServeHTTP(w, r)

View File

@ -1,18 +1,28 @@
package middleware
import (
"log/slog"
"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/valuer"
)
const (
authCrossOrgMessage string = "::AUTH-CROSS-ORG::"
)
type Auth struct {
jwt *authtypes.JWT
headers []string
sharder sharder.Sharder
logger *slog.Logger
}
func NewAuth(jwt *authtypes.JWT, headers []string) *Auth {
return &Auth{jwt: jwt, headers: headers}
func NewAuth(jwt *authtypes.JWT, headers []string, sharder sharder.Sharder, logger *slog.Logger) *Auth {
return &Auth{jwt: jwt, headers: headers, sharder: sharder, logger: logger}
}
func (a *Auth) Wrap(next http.Handler) http.Handler {
@ -28,6 +38,18 @@ func (a *Auth) Wrap(next http.Handler) http.Handler {
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)
next.ServeHTTP(w, r)

View File

@ -4,13 +4,13 @@ import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/apdextypes"
)
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 {

View File

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

View File

@ -6,7 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"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/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) {
var apdexSettings []*types.ApdexSettings
func (module *module) Get(ctx context.Context, orgID string, services []string) ([]*apdextypes.Settings, error) {
var apdexSettings []*apdextypes.Settings
err := module.
sqlstore.
@ -51,7 +51,7 @@ func (module *module) Get(ctx context.Context, orgID string, services []string)
}
if !found {
apdexSettings = append(apdexSettings, &types.ApdexSettings{
apdexSettings = append(apdexSettings, &apdextypes.Settings{
ServiceName: service,
Threshold: defaultApdexThreshold,
})
@ -61,7 +61,7 @@ func (module *module) Get(ctx context.Context, orgID string, services []string)
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.Identifiable.ID = valuer.GenerateUUID()

View File

@ -4,25 +4,32 @@ import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
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 {
Create(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
LockUnlock(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
}

View File

@ -2,12 +2,16 @@ package impldashboard
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@ -19,8 +23,8 @@ func NewHandler(module dashboard.Module) dashboard.Handler {
return &handler{module: module}
}
func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
@ -29,13 +33,151 @@ func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) {
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 {
render.Error(rw, err)
return
}
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 (
"context"
"encoding/json"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/google/uuid"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
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{
sqlstore: sqlstore,
store: NewStore(sqlstore),
settings: scopedProviderSettings,
}
}
// CreateDashboard creates a new dashboard
func (module *module) Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error) {
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)
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
if err != nil {
return nil, err
}
return dashboards, nil
}
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)
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return nil, err
}
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)
err = module.store.Create(ctx, storableDashboard)
if err != nil {
return nil, err
}
@ -167,28 +43,73 @@ func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string,
return dashboard, nil
}
func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error {
dashboard, err := module.Get(ctx, orgID, uuid)
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
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 {
return err
}
var lockValue int
if lock {
lockValue = 1
} else {
lockValue = 0
err = dashboard.LockUnlock(ctx, lock, updatedBy)
if err != nil {
return err
}
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return err
}
_, err = module.
sqlstore.
BunDB().
NewUpdate().
Model(dashboard).
Set("locked = ?", lockValue).
Where("org_id = ?", orgID).
Where("uuid = ?", uuid).
Exec(ctx)
err = module.store.Update(ctx, orgID, storableDashboard)
if err != nil {
return err
}
@ -196,15 +117,21 @@ func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock b
return nil
}
func (module *module) GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error) {
dashboards := []types.Dashboard{}
err := module.
sqlstore.
BunDB().
NewSelect().
Model(&dashboards).
Where("org_id = ?", orgID).
Scan(ctx)
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil {
return err
}
if dashboard.Locked {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}
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 {
return nil, err
}
@ -266,7 +193,7 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
for _, metricName := range metricNames {
if strings.TrimSpace(key) == metricName {
result[metricName] = append(result[metricName], map[string]string{
"dashboard_id": dashboard.UUID,
"dashboard_id": dashboard.ID,
"widget_name": widgetTitle,
"widget_id": widgetID,
"dashboard_name": dashTitle,
@ -280,52 +207,3 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
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 {
module organization.Module
orgGetter organization.Getter
orgSetter organization.Setter
}
func NewHandler(module organization.Module) organization.Handler {
return &handler{module: module}
func NewHandler(orgGetter organization.Getter, orgSetter organization.Setter) organization.Handler {
return &handler{orgGetter: orgGetter, orgSetter: orgSetter}
}
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
}
organization, err := handler.module.Get(ctx, orgID)
organization, err := handler.orgGetter.Get(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
@ -67,10 +68,11 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
req.ID = orgID
err = handler.module.Update(ctx, req)
err = handler.orgSetter.Update(ctx, req)
if err != nil {
render.Error(rw, err)
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
}
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"
)
type Module interface {
// Create creates the given organization
Create(context.Context, *types.Organization) error
type Getter interface {
// Get gets the organization based on the given id
Get(context.Context, valuer.UUID) (*types.Organization, error)
// GetAll gets all the organizations
GetAll(context.Context) ([]*types.Organization, error)
// Lists all the organizations
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
}

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/errors"
"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/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@ -22,20 +24,22 @@ import (
)
type Module struct {
store types.UserStore
jwt *authtypes.JWT
emailing emailing.Emailing
settings factory.ScopedProviderSettings
store types.UserStore
jwt *authtypes.JWT
emailing emailing.Emailing
settings factory.ScopedProviderSettings
orgSetter organization.Setter
}
// 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")
return &Module{
store: store,
jwt: jwt,
emailing: emailing,
settings: settings,
store: store,
jwt: jwt,
emailing: emailing,
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 {
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)
RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) 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 {

View File

@ -4,11 +4,13 @@ import (
"context"
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"math"
"strconv"
"strings"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/telemetrystore"
promValue "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage"
"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 timestampMs int64
var value float64
var flags uint32
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
}
@ -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
ts.Samples = append(ts.Samples, prompb.Sample{
Timestamp: timestampMs,

View File

@ -6267,9 +6267,6 @@ func (r *ClickHouseReader) GetUpdatedMetricsMetadata(ctx context.Context, orgID
if err == nil {
cachedMetadata[metricName] = metadata
} 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)
}
}

View File

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

View File

@ -3,17 +3,23 @@ package cloudintegrations
import (
"context"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
"github.com/SigNoz/signoz/pkg/alertmanager"
"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/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"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/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/authtypes"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
@ -24,11 +30,16 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
controller, err := NewController(sqlStore)
require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
user, apiErr := createTestUser(organizationModule, userModule)
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
require.NoError(err)
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)
// should be able to generate connection url for
@ -74,11 +85,17 @@ func TestAgentCheckIns(t *testing.T) {
sqlStore := utils.NewQueryServiceDBForTests(t)
controller, err := NewController(sqlStore)
require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
user, apiErr := createTestUser(organizationModule, userModule)
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
require.NoError(err)
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)
// 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)
require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
user, apiErr := createTestUser(organizationModule, userModule)
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
require.NoError(err)
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)
// Attempting to disconnect a non-existent account should return error
@ -186,11 +208,16 @@ func TestConfigureService(t *testing.T) {
controller, err := NewController(sqlStore)
require.NoError(err)
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
providerSettings := instrumentationtest.New().ToProviderSettings()
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
user, apiErr := createTestUser(organizationModule, userModule)
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
require.NoError(err)
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)
// create a connected account
@ -305,7 +332,7 @@ func makeTestConnectedAccount(t *testing.T, orgId string, controller *Controller
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
ctx := context.Background()
organization := types.NewOrganization("test")

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