Compare commits

..

4 Commits

Author SHA1 Message Date
nityanandagohain
b43061fff1 feat: system dashboards 2026-04-12 21:57:53 +05:30
swapnil-signoz
7279c5f770 feat: adding query params in cloud integration APIs (#10900)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* feat: adding query params in cloud integration APIs

* refactor: create account HTTP status change from OK to CREATED
2026-04-10 09:20:35 +00:00
Nikhil Soni
e543776efc chore: send obfuscate query in the clickhouse query panel update (#10848)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: send query in the clickhouse query panel update

* chore: obfuscate query to avoid sending sensitive values
2026-04-09 14:15:10 +00:00
Pandey
621127b7fb feat(audit): wire auditor into DI graph and service lifecycle (#10891)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(audit): wire auditor into DI graph and service lifecycle

Register the auditor in the factory service registry so it participates
in application lifecycle (start/stop/health). Community uses noopauditor,
enterprise uses otlphttpauditor with licensing gate. Pass the auditor
instance to the audit middleware instead of nil.

* feat(audit): use NamedMap provider pattern with config-driven selection

Switch from single-factory callback to NamedMap + factory.NewProviderFromNamedMap
so the config's Provider field selects the auditor implementation. Add
NewAuditorProviderFactories() with noop as the community default. Enterprise
extends the map with otlphttpauditor. Add auditor section to conf/example.yaml
and set default provider to "noop" in config.

* chore: move auditor config to end of example.yaml
2026-04-09 11:44:05 +00:00
44 changed files with 827 additions and 281 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
@@ -93,6 +94,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()
},
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return signoz.NewAuditorProviderFactories()
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
return querier.NewHandler(ps, q, a)
},

View File

@@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/auditor/otlphttpauditor"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
@@ -24,6 +25,7 @@ import (
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/errors"
@@ -133,6 +135,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)
},
func(licensing licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
factories := signoz.NewAuditorProviderFactories()
if err := factories.Add(otlphttpauditor.NewFactory(licensing, version.Info)); err != nil {
panic(err)
}
return factories
},
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
communityHandler := querier.NewHandler(ps, q, a)
return eequerier.NewHandler(ps, q, communityHandler)

View File

@@ -364,3 +364,34 @@ serviceaccount:
analytics:
# toggle service account analytics
enabled: true
##################### Auditor #####################
auditor:
# Specifies the auditor provider to use.
# noop: discards all audit events (community default).
# otlphttp: exports audit events via OTLP HTTP (enterprise).
provider: noop
# The async channel capacity for audit events. Events are dropped when full (fail-open).
buffer_size: 1000
# The maximum number of events per export batch.
batch_size: 100
# The maximum time between export flushes.
flush_interval: 1s
otlphttp:
# The target scheme://host:port/path of the OTLP HTTP endpoint.
endpoint: http://localhost:4318/v1/logs
# Whether to use HTTP instead of HTTPS.
insecure: false
# The maximum duration for an export attempt.
timeout: 10s
# Additional HTTP headers sent with every export request.
headers: {}
retry:
# Whether to retry on transient failures.
enabled: true
# The initial wait time before the first retry.
initial_interval: 5s
# The upper bound on backoff interval.
max_interval: 30s
# The total maximum time spent retrying.
max_elapsed_time: 60s

View File

@@ -2698,6 +2698,37 @@ components:
- name
- expiresAt
type: object
SystemdashboardtypesData:
additionalProperties: {}
type: object
SystemdashboardtypesGettableSystemDashboard:
properties:
created_at:
format: date-time
type: string
data:
$ref: '#/components/schemas/SystemdashboardtypesData'
id:
type: string
source:
type: string
updated_at:
format: date-time
type: string
required:
- id
- source
- data
- created_at
- updated_at
type: object
SystemdashboardtypesUpdatableSystemDashboard:
properties:
data:
$ref: '#/components/schemas/SystemdashboardtypesData'
required:
- data
type: object
TelemetrytypesFieldContext:
enum:
- metric
@@ -3309,7 +3340,7 @@ paths:
schema:
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
responses:
"200":
"201":
content:
application/json:
schema:
@@ -3322,7 +3353,7 @@ paths:
- status
- data
type: object
description: OK
description: Created
"401":
content:
application/json:
@@ -3683,6 +3714,11 @@ paths:
provider
operationId: ListServicesMetadata
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true
@@ -3735,6 +3771,11 @@ paths:
description: This endpoint gets a service for the specified cloud provider
operationId: GetService
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true
@@ -6424,6 +6465,133 @@ paths:
summary: Updates my service account
tags:
- serviceaccount
/api/v1/system/{source}/dashboard:
get:
deprecated: false
description: Returns the system dashboard for the given source. Seeds default
panels on the first call if no dashboard exists yet.
operationId: GetSystemDashboard
parameters:
- in: path
name: source
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SystemdashboardtypesGettableSystemDashboard'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get a system dashboard
tags:
- system-dashboard
put:
deprecated: false
description: Replaces the panels, layout, and variables of a system dashboard.
operationId: UpdateSystemDashboard
parameters:
- in: path
name: source
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SystemdashboardtypesUpdatableSystemDashboard'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SystemdashboardtypesGettableSystemDashboard'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Update a system dashboard
tags:
- system-dashboard
/api/v1/user:
get:
deprecated: false

View File

@@ -227,7 +227,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)

View File

@@ -28,7 +28,7 @@ import type {
CloudintegrationtypesPostableAgentCheckInDTO,
CloudintegrationtypesUpdatableAccountDTO,
CloudintegrationtypesUpdatableServiceDTO,
CreateAccount200,
CreateAccount201,
CreateAccountPathParameters,
DisconnectAccountPathParameters,
GetAccount200,
@@ -36,10 +36,12 @@ import type {
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
GetServiceParams,
GetServicePathParameters,
ListAccounts200,
ListAccountsPathParameters,
ListServicesMetadata200,
ListServicesMetadataParams,
ListServicesMetadataPathParameters,
RenderErrorResponseDTO,
UpdateAccountPathParameters,
@@ -260,7 +262,7 @@ export const createAccount = (
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAccount200>({
return GeneratedAPIInstance<CreateAccount201>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -940,19 +942,25 @@ export const invalidateGetConnectionCredentials = async (
*/
export const listServicesMetadata = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListServicesMetadata200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
method: 'GET',
params,
signal,
});
};
export const getListServicesMetadataQueryKey = ({
cloudProvider,
}: ListServicesMetadataPathParameters) => {
return [`/api/v1/cloud_integrations/${cloudProvider}/services`] as const;
export const getListServicesMetadataQueryKey = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services`,
...(params ? [params] : []),
] as const;
};
export const getListServicesMetadataQueryOptions = <
@@ -960,6 +968,7 @@ export const getListServicesMetadataQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -971,11 +980,12 @@ export const getListServicesMetadataQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListServicesMetadataQueryKey({ cloudProvider });
queryOptions?.queryKey ??
getListServicesMetadataQueryKey({ cloudProvider }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listServicesMetadata>>
> = ({ signal }) => listServicesMetadata({ cloudProvider }, signal);
> = ({ signal }) => listServicesMetadata({ cloudProvider }, params, signal);
return {
queryKey,
@@ -1003,6 +1013,7 @@ export function useListServicesMetadata<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -1013,6 +1024,7 @@ export function useListServicesMetadata<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListServicesMetadataQueryOptions(
{ cloudProvider },
params,
options,
);
@@ -1031,10 +1043,11 @@ export function useListServicesMetadata<
export const invalidateListServicesMetadata = async (
queryClient: QueryClient,
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }) },
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }, params) },
options,
);
@@ -1047,21 +1060,24 @@ export const invalidateListServicesMetadata = async (
*/
export const getService = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
method: 'GET',
params,
signal,
});
};
export const getGetServiceQueryKey = ({
cloudProvider,
serviceId,
}: GetServicePathParameters) => {
export const getGetServiceQueryKey = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
...(params ? [params] : []),
] as const;
};
@@ -1070,6 +1086,7 @@ export const getGetServiceQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1081,11 +1098,12 @@ export const getGetServiceQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceQueryKey({ cloudProvider, serviceId });
queryOptions?.queryKey ??
getGetServiceQueryKey({ cloudProvider, serviceId }, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
signal,
}) => getService({ cloudProvider, serviceId }, signal);
}) => getService({ cloudProvider, serviceId }, params, signal);
return {
queryKey,
@@ -1111,6 +1129,7 @@ export function useGetService<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1121,6 +1140,7 @@ export function useGetService<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceQueryOptions(
{ cloudProvider, serviceId },
params,
options,
);
@@ -1139,10 +1159,11 @@ export function useGetService<
export const invalidateGetService = async (
queryClient: QueryClient,
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }) },
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }, params) },
options,
);

View File

@@ -3589,7 +3589,7 @@ export type ListAccounts200 = {
export type CreateAccountPathParameters = {
cloudProvider: string;
};
export type CreateAccount200 = {
export type CreateAccount201 = {
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
/**
* @type string
@@ -3647,6 +3647,14 @@ export type GetConnectionCredentials200 = {
export type ListServicesMetadataPathParameters = {
cloudProvider: string;
};
export type ListServicesMetadataParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type ListServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
@@ -3659,6 +3667,14 @@ export type GetServicePathParameters = {
cloudProvider: string;
serviceId: string;
};
export type GetServiceParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type GetService200 = {
data: CloudintegrationtypesServiceDTO;
/**

View File

@@ -25,12 +25,11 @@ export const REACT_QUERY_KEY = {
ALERT_RULE_TIMELINE_GRAPH: 'ALERT_RULE_TIMELINE_GRAPH',
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
TOGGLE_ALERT_STATE: 'TOGGLE_ALERT_STATE',
GET_ALL_ALERTS: 'GET_ALL_ALERTS',
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
ALERT_RULES_CHART_PREVIEW: 'ALERT_RULES_CHART_PREVIEW',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH',

View File

@@ -1,11 +1,9 @@
import { useCallback, useMemo } from 'react';
import { useIsFetching, useQueryClient } from 'react-query';
import { Button } from 'antd';
import classNames from 'classnames';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getMetricNameFromQueryData } from 'hooks/useGetYAxisUnit';
@@ -64,13 +62,6 @@ function QuerySection(): JSX.Element {
return currentQueryKey !== stagedQueryKey;
}, [currentQuery, alertType, thresholdState, stagedQuery]);
const queryClient = useQueryClient();
const isLoadingQueries =
useIsFetching([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]) > 0;
const handleCancelQuery = useCallback(() => {
queryClient.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
}, [queryClient]);
const runQueryHandler = useCallback(() => {
// Reset the source param when the query is changed
// Then manually run the query
@@ -139,8 +130,6 @@ function QuerySection(): JSX.Element {
setQueryCategory={onQueryCategoryChange}
alertType={alertType}
runQuery={runQueryHandler}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
alertDef={alertDef}
panelType={PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}

View File

@@ -10,7 +10,6 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
import { Threshold } from 'container/CreateAlertV2/context/types';
@@ -186,7 +185,7 @@ function ChartPreview({
ENTITY_VERSION_V5,
{
queryKey: [
REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW,
'chartPreview',
userQueryKey || JSON.stringify(query),
selectedInterval,
minTime,

View File

@@ -29,8 +29,6 @@ function QuerySection({
setQueryCategory,
alertType,
runQuery,
isLoadingQueries,
handleCancelQuery,
alertDef,
panelType,
ruleId,
@@ -178,8 +176,6 @@ function QuerySection({
queryType: queryCategory,
});
}}
handleCancelQuery={handleCancelQuery}
isLoadingQueries={isLoadingQueries}
/>
</span>
}
@@ -199,11 +195,7 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<RunQueryBtn
onStageRunQuery={runQuery}
handleCancelQuery={handleCancelQuery}
isLoadingQueries={isLoadingQueries}
/>
<RunQueryBtn onStageRunQuery={runQuery} />
</span>
}
items={items}
@@ -245,8 +237,6 @@ interface QuerySectionProps {
setQueryCategory: (n: EQueryType) => void;
alertType: AlertTypes;
runQuery: VoidFunction;
isLoadingQueries: boolean;
handleCancelQuery: () => void;
alertDef: AlertDef;
panelType: PANEL_TYPES;
ruleId: string;

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useIsFetching, useQueryClient } from 'react-query';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@@ -127,11 +127,6 @@ function FormAlertRules({
// use query client
const ruleCache = useQueryClient();
const isLoadingAlertQuery =
useIsFetching([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]) > 0;
const handleCancelAlertQuery = useCallback(() => {
ruleCache.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
}, [ruleCache]);
const isNewRule = !ruleId || isEmpty(ruleId);
@@ -919,8 +914,6 @@ function FormAlertRules({
setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={(): void => handleRunQuery()}
isLoadingQueries={isLoadingAlertQuery}
handleCancelQuery={handleCancelAlertQuery}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}

View File

@@ -0,0 +1,43 @@
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
return (
<div className="query-section">
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
version="v4"
actions={
<ButtonWrapper>
<Button
onClick={(): void => {
handleRunQuery();
logEvent(MeterExplorerEvents.QueryBuilderQueryChanged, {
[MeterExplorerEventKeys.Tab]: 'explorer',
});
}}
type="primary"
>
Run Query
</Button>
</ButtonWrapper>
}
/>
</div>
);
}
export default QuerySection;

View File

@@ -1,10 +1,7 @@
import { useCallback } from 'react';
import { useIsFetching, useQueryClient } from 'react-query';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { QueryBuilder } from 'container/QueryBuilder';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -14,16 +11,9 @@ import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
const queryClient = useQueryClient();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
const isLoadingQueries = useIsFetching([REACT_QUERY_KEY.GET_QUERY_RANGE]) > 0;
const handleCancelQuery = useCallback(() => {
queryClient.cancelQueries([REACT_QUERY_KEY.GET_QUERY_RANGE]);
}, [queryClient]);
return (
<div className="query-section">
<QueryBuilder
@@ -32,16 +22,17 @@ function QuerySection(): JSX.Element {
version="v4"
actions={
<ButtonWrapper>
<RunQueryBtn
onStageRunQuery={(): void => {
<Button
onClick={(): void => {
handleRunQuery();
logEvent(MetricsExplorerEvents.QueryBuilderQueryChanged, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
});
}}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
/>
type="primary"
>
Run Query
</Button>
</ButtonWrapper>
}
/>

View File

@@ -1,11 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricMetadata } from 'api/generated/services/metrics';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -111,11 +109,6 @@ function Inspect({
reset,
} = useInspectMetrics(appliedMetricName);
const queryClient = useQueryClient();
const handleCancelInspectQuery = useCallback(() => {
queryClient.cancelQueries(REACT_QUERY_KEY.GET_INSPECT_METRICS_DETAILS);
}, [queryClient]);
const handleDispatchMetricInspectionOptions = useCallback(
(action: MetricInspectionAction): void => {
dispatchMetricInspectionOptions(action);
@@ -241,8 +234,6 @@ function Inspect({
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
currentQuery={currentQueryData}
setCurrentQuery={setCurrentQueryData}
isLoadingQueries={isInspectMetricsLoading}
handleCancelQuery={handleCancelInspectQuery}
/>
</div>
<div className="inspect-metrics-content-second-col">

View File

@@ -20,8 +20,6 @@ function QueryBuilder({
inspectMetricsTimeSeries,
currentQuery,
setCurrentQuery,
isLoadingQueries,
handleCancelQuery,
}: QueryBuilderProps): JSX.Element {
const applyInspectionOptions = useCallback(() => {
setAppliedMetricName(currentMetricName ?? '');
@@ -41,11 +39,7 @@ function QueryBuilder({
>
Query Builder
</Button>
<RunQueryBtn
onStageRunQuery={applyInspectionOptions}
handleCancelQuery={handleCancelQuery}
isLoadingQueries={isLoadingQueries}
/>
<RunQueryBtn onStageRunQuery={applyInspectionOptions} />
</div>
<Card className="inspect-metrics-query-builder-content">
<MetricNameSearch

View File

@@ -103,8 +103,6 @@ describe('QueryBuilder', () => {
filterExpression: '',
} as any,
setCurrentQuery: jest.fn(),
isLoadingQueries: false,
handleCancelQuery: jest.fn(),
};
beforeEach(() => {

View File

@@ -65,8 +65,6 @@ export interface QueryBuilderProps {
inspectMetricsTimeSeries: InspectMetricsSeries[];
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
isLoadingQueries: boolean;
handleCancelQuery: () => void;
}
export interface MetricNameSearchProps {

View File

@@ -12,8 +12,6 @@ function MetricsSearch({
currentQueryFilterExpression,
setCurrentQueryFilterExpression,
isLoading,
handleCancelQuery,
onRunQuery,
}: MetricsSearchProps): JSX.Element {
const handleOnChange = useCallback(
(expression: string): void => {
@@ -24,8 +22,7 @@ function MetricsSearch({
const handleStageAndRunQuery = useCallback(() => {
onChange(currentQueryFilterExpression);
onRunQuery?.();
}, [currentQueryFilterExpression, onChange, onRunQuery]);
}, [currentQueryFilterExpression, onChange]);
const handleRunQuery = useCallback(
(expression: string): void => {
@@ -56,7 +53,6 @@ function MetricsSearch({
<RunQueryBtn
onStageRunQuery={handleStageAndRunQuery}
isLoadingQueries={isLoading}
handleCancelQuery={handleCancelQuery}
/>
<div className="metrics-search-options">
<DateTimeSelectionV2

View File

@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
@@ -105,8 +104,6 @@ function Summary(): JSX.Element {
setCurrentQueryFilterExpression,
] = useState<string>(appliedFilterExpression);
const [isCancelled, setIsCancelled] = useState<boolean>(false);
useEffect(() => {
setCurrentQueryFilterExpression(appliedFilterExpression);
}, [appliedFilterExpression]);
@@ -167,7 +164,6 @@ function Summary(): JSX.Element {
isLoading: isGetMetricsStatsLoading,
isError: isGetMetricsStatsError,
error: metricsStatsError,
reset: resetMetricsStats,
} = useGetMetricsStats();
const {
@@ -176,7 +172,6 @@ function Summary(): JSX.Element {
isLoading: isGetMetricsTreemapLoading,
isError: isGetMetricsTreemapError,
error: metricsTreemapError,
reset: resetMetricsTreemap,
} = useGetMetricsTreemap();
const metricsStatsApiError = useMemo(
@@ -201,40 +196,6 @@ function Summary(): JSX.Element {
});
}, [metricsTreemapQuery, getMetricsTreemap]);
const handleCancelQuery = useCallback(() => {
resetMetricsStats();
resetMetricsTreemap();
setCurrentQueryFilterExpression(appliedFilterExpression);
setIsCancelled(true);
}, [
resetMetricsStats,
resetMetricsTreemap,
setCurrentQueryFilterExpression,
appliedFilterExpression,
]);
const handleRunQuery = useCallback(() => {
setIsCancelled(false);
getMetricsStats({
data: {
...metricsListQuery,
filter: { expression: currentQueryFilterExpression },
},
});
getMetricsTreemap({
data: {
...metricsTreemapQuery,
filter: { expression: currentQueryFilterExpression },
},
});
}, [
getMetricsStats,
getMetricsTreemap,
metricsListQuery,
metricsTreemapQuery,
currentQueryFilterExpression,
]);
const handleFilterChange = useCallback(
(expression: string) => {
const newFilters: TagFilter = {
@@ -369,19 +330,11 @@ function Summary(): JSX.Element {
!isGetMetricsTreemapLoading &&
!isGetMetricsTreemapError;
const isLoadingQueries =
isGetMetricsStatsLoading || isGetMetricsTreemapLoading;
const showFullScreenLoading =
isLoadingQueries &&
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
formattedMetricsData.length === 0 &&
!treeMapData?.data[heatmapView]?.length;
const showNoMetrics =
isMetricsListDataEmpty &&
isMetricsTreeMapDataEmpty &&
!appliedFilterExpression;
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
@@ -390,30 +343,13 @@ function Summary(): JSX.Element {
onChange={handleFilterChange}
currentQueryFilterExpression={currentQueryFilterExpression}
setCurrentQueryFilterExpression={setCurrentQueryFilterExpression}
isLoading={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
onRunQuery={handleRunQuery}
isLoading={isGetMetricsStatsLoading || isGetMetricsTreemapLoading}
/>
{showFullScreenLoading ? (
<MetricsLoading />
) : isCancelled ? (
<div className="no-logs-container">
<div className="no-logs-container-content">
<img
className="eyes-emoji"
src="/Images/eyesEmoji.svg"
alt="eyes emoji"
/>
<Typography className="no-logs-text">
Query cancelled.
<span className="sub-text">
{' '}
Click &quot;Run Query&quot; to load metrics.
</span>
</Typography>
</div>
</div>
) : showNoMetrics ? (
) : isMetricsListDataEmpty &&
isMetricsTreeMapDataEmpty &&
!appliedFilterExpression ? (
<NoLogs dataSource={DataSource.METRICS} />
) : (
<>

View File

@@ -33,8 +33,6 @@ export interface MetricsSearchProps {
currentQueryFilterExpression: string;
setCurrentQueryFilterExpression: (expression: string) => void;
isLoading: boolean;
handleCancelQuery: () => void;
onRunQuery: () => void;
}
export interface MetricsTreemapProps {

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react';
import { QueryKey } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -24,8 +25,8 @@ import PromQLQueryContainer from './QueryBuilder/promQL';
import './QuerySection.styles.scss';
function QuerySection({
selectedGraph,
queryRangeKey,
isLoadingQueries,
handleCancelQuery,
selectedWidget,
dashboardVersion,
dashboardId,
@@ -178,7 +179,7 @@ function QuerySection({
label="Stage & Run Query"
onStageRunQuery={handleRunQuery}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
queryRangeKey={queryRangeKey}
/>
</span>
}
@@ -190,8 +191,8 @@ function QuerySection({
interface QueryProps {
selectedGraph: PANEL_TYPES;
isLoadingQueries: boolean;
handleCancelQuery: () => void;
queryRangeKey?: QueryKey;
isLoadingQueries?: boolean;
selectedWidget: Widgets;
dashboardVersion?: string;
dashboardId?: string;

View File

@@ -1,5 +1,5 @@
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useQueryClient } from 'react-query';
import { memo, useEffect } from 'react';
import { useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { ENTITY_VERSION_V5 } from 'constants/app';
@@ -34,7 +34,6 @@ function LeftContainer({
isNewPanel = false,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const queryClient = useQueryClient();
const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector<
AppState,
@@ -50,10 +49,6 @@ function LeftContainer({
],
[globalSelectedInterval, requestData, minTime, maxTime],
);
const handleCancelQuery = useCallback(() => {
queryClient.cancelQueries(queryRangeKey);
}, [queryClient, queryRangeKey]);
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
enabled: !!stagedQuery,
queryKey: queryRangeKey,
@@ -80,8 +75,8 @@ function LeftContainer({
<QueryContainer className="query-section-left-container">
<QuerySection
selectedGraph={selectedGraph}
queryRangeKey={queryRangeKey}
isLoadingQueries={queryResponse.isFetching}
handleCancelQuery={handleCancelQuery}
selectedWidget={selectedWidget}
dashboardVersion={ENTITY_VERSION_V5}
dashboardId={selectedDashboard?.id}

View File

@@ -677,6 +677,18 @@ function NewWidget({
queryType: currentQuery.queryType,
isNewPanel,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
...(currentQuery.queryType === EQueryType.CLICKHOUSE && {
clickhouseQueryCount: currentQuery.clickhouse_sql.length,
clickhouseQueries: currentQuery.clickhouse_sql.map((q) => ({
name: q.name,
query: (q.query ?? '')
.replace(/--[^\n]*/g, '') // strip line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // strip block comments
.replace(/'(?:[^'\\]|\\.|'')*'/g, "'?'") // replace single-quoted strings (handles \' and '' escapes)
.replace(/\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g, '?'), // replace numeric literals (int, float, scientific)
disabled: q.disabled,
})),
}),
});
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -1,3 +1,5 @@
import { useCallback } from 'react';
import { QueryKey, useIsFetching, useQueryClient } from 'react-query';
import { Button } from 'antd';
import cx from 'classnames';
import {
@@ -10,23 +12,14 @@ import {
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import './RunQueryBtn.scss';
type RunQueryBtnProps = {
interface RunQueryBtnProps {
className?: string;
label?: string;
disabled?: boolean;
} & (
| {
onStageRunQuery: () => void;
handleCancelQuery: () => void;
isLoadingQueries: boolean;
}
| {
onStageRunQuery?: never;
handleCancelQuery?: never;
isLoadingQueries?: never;
}
);
isLoadingQueries?: boolean;
handleCancelQuery?: () => void;
onStageRunQuery?: () => void;
queryRangeKey?: QueryKey;
}
function RunQueryBtn({
className,
@@ -34,17 +27,33 @@ function RunQueryBtn({
isLoadingQueries,
handleCancelQuery,
onStageRunQuery,
disabled,
queryRangeKey,
}: RunQueryBtnProps): JSX.Element {
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
const isLoading = isLoadingQueries ?? false;
const queryClient = useQueryClient();
const isKeyFetchingCount = useIsFetching(
queryRangeKey as QueryKey | undefined,
);
const isLoading =
typeof isLoadingQueries === 'boolean'
? isLoadingQueries
: isKeyFetchingCount > 0;
const onCancel = useCallback(() => {
if (handleCancelQuery) {
return handleCancelQuery();
}
if (queryRangeKey) {
queryClient.cancelQueries(queryRangeKey);
}
}, [handleCancelQuery, queryClient, queryRangeKey]);
return isLoading ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className={cx('cancel-query-btn periscope-btn danger', className)}
onClick={handleCancelQuery}
onClick={onCancel}
>
Cancel
</Button>
@@ -52,7 +61,7 @@ function RunQueryBtn({
<Button
type="primary"
className={cx('run-query-btn periscope-btn primary', className)}
disabled={disabled}
disabled={isLoading || !onStageRunQuery}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>

View File

@@ -1,9 +1,18 @@
// frontend/src/container/QueryBuilder/components/RunQueryBtn/__tests__/RunQueryBtn.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen } from '@testing-library/react';
import RunQueryBtn from '../RunQueryBtn';
jest.mock('react-query', () => {
const actual = jest.requireActual('react-query');
return {
...actual,
useIsFetching: jest.fn(),
useQueryClient: jest.fn(),
};
});
import { useIsFetching, useQueryClient } from 'react-query';
// Mock OS util
jest.mock('utils/getUserOS', () => ({
getUserOperatingSystem: jest.fn(),
@@ -17,76 +26,79 @@ describe('RunQueryBtn', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
(useIsFetching as jest.Mock).mockReturnValue(0);
(useQueryClient as jest.Mock).mockReturnValue({
cancelQueries: jest.fn(),
});
});
test('shows run button and hides cancel when isLoadingQueries is false', () => {
render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
);
expect(screen.getByRole('button', { name: /run query/i })).toBeEnabled();
expect(
screen.queryByRole('button', { name: /cancel/i }),
).not.toBeInTheDocument();
});
test('shows cancel button and hides run button when isLoadingQueries is true', () => {
render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries
/>,
);
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /run query/i }),
).not.toBeInTheDocument();
});
test('calls onStageRunQuery when run button is clicked', async () => {
const user = userEvent.setup();
test('uses isLoadingQueries prop over useIsFetching', () => {
// Simulate fetching but prop forces not loading
(useIsFetching as jest.Mock).mockReturnValue(1);
const onRun = jest.fn();
render(
<RunQueryBtn
onStageRunQuery={onRun}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
);
await user.click(screen.getByRole('button', { name: /run query/i }));
render(<RunQueryBtn onStageRunQuery={onRun} isLoadingQueries={false} />);
// Should show "Run Query" (not cancel)
const runBtn = screen.getByRole('button', { name: /run query/i });
expect(runBtn).toBeInTheDocument();
expect(runBtn).toBeEnabled();
});
test('fallback cancel: uses handleCancelQuery when no key provided', () => {
(useIsFetching as jest.Mock).mockReturnValue(0);
const cancelQueries = jest.fn();
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelBtn);
expect(onCancel).toHaveBeenCalledTimes(1);
expect(cancelQueries).not.toHaveBeenCalled();
});
test('renders run state and triggers on click', () => {
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} />);
const btn = screen.getByRole('button', { name: /run query/i });
expect(btn).toBeEnabled();
fireEvent.click(btn);
expect(onRun).toHaveBeenCalledTimes(1);
});
test('calls handleCancelQuery when cancel button is clicked', async () => {
const user = userEvent.setup();
const onCancel = jest.fn();
render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
isLoadingQueries
handleCancelQuery={onCancel}
/>,
);
await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
test('is disabled when no props provided', () => {
test('disabled when onStageRunQuery is undefined', () => {
render(<RunQueryBtn />);
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
});
test('shows cancel state and calls handleCancelQuery', () => {
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancel = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancel);
expect(onCancel).toHaveBeenCalledTimes(1);
});
test('derives loading from queryKey via useIsFetching and cancels via queryClient', () => {
(useIsFetching as jest.Mock).mockReturnValue(1);
const cancelQueries = jest.fn();
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
const queryKey = ['GET_QUERY_RANGE', '1h', { some: 'req' }, 1, 2];
render(<RunQueryBtn queryRangeKey={queryKey} />);
// Button switches to cancel state
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
expect(cancelBtn).toBeInTheDocument();
// Clicking cancel calls cancelQueries with the key
fireEvent.click(cancelBtn);
expect(cancelQueries).toHaveBeenCalledWith(queryKey);
});
test('shows Command + CornerDownLeft on mac', () => {
const { container } = render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-command')).toBeInTheDocument();
expect(
@@ -99,11 +111,7 @@ describe('RunQueryBtn', () => {
UserOperatingSystem.WINDOWS,
);
const { container } = render(
<RunQueryBtn
onStageRunQuery={jest.fn()}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
/>,
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
expect(container.querySelector('.lucide-chevron-up')).toBeInTheDocument();
expect(container.querySelector('.lucide-command')).not.toBeInTheDocument();
@@ -112,20 +120,11 @@ describe('RunQueryBtn', () => {
).toBeInTheDocument();
});
test('renders custom label and calls onStageRunQuery on click', async () => {
const user = userEvent.setup();
test('renders custom label when provided', () => {
const onRun = jest.fn();
render(
<RunQueryBtn
onStageRunQuery={onRun}
handleCancelQuery={jest.fn()}
isLoadingQueries={false}
label="Stage & Run Query"
/>,
);
const btn = screen.getByRole('button', { name: /stage & run query/i });
expect(btn).toBeInTheDocument();
await user.click(btn);
expect(onRun).toHaveBeenCalledTimes(1);
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
expect(
screen.getByRole('button', { name: /stage & run query/i }),
).toBeInTheDocument();
});
});

View File

@@ -42,7 +42,7 @@ export default function RightToolbarActions({
if (showLiveLogs) {
return (
<div className="right-toolbar-actions-container">
<RunQueryBtn disabled />
<RunQueryBtn />
</div>
);
}
@@ -59,7 +59,7 @@ export default function RightToolbarActions({
return (
<div className="right-toolbar-actions-container">
<RunQueryBtn
isLoadingQueries={!!isLoadingQueries}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
onStageRunQuery={onStageRunQuery}
/>

View File

@@ -431,7 +431,7 @@ export const useAlertRuleDuplicate = ({
const params = useUrlQuery();
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALERTS, {
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALLERTS, {
queryFn: getAll,
cacheTime: 0,
});

View File

@@ -41,7 +41,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
RequestContentType: "application/json",
Response: new(citypes.GettableAccountWithConnectionArtifact),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
@@ -138,6 +138,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Summary: "List services metadata",
Description: "This endpoint lists the services metadata for the specified cloud provider",
Request: nil,
RequestQuery: new(citypes.ListServicesMetadataParams),
RequestContentType: "",
Response: new(citypes.GettableServicesMetadata),
ResponseContentType: "application/json",
@@ -158,6 +159,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Summary: "Get service",
Description: "This endpoint gets a service for the specified cloud provider",
Request: nil,
RequestQuery: new(citypes.GetServiceParams),
RequestContentType: "",
Response: new(citypes.Service),
ResponseContentType: "application/json",

View File

@@ -22,6 +22,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -57,6 +58,7 @@ type provider struct {
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
systemDashboardHandler systemdashboard.Handler
}
func NewFactory(
@@ -83,6 +85,7 @@ func NewFactory(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
systemDashboardHandler systemdashboard.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -112,6 +115,7 @@ func NewFactory(
factoryHandler,
cloudIntegrationHandler,
ruleStateHistoryHandler,
systemDashboardHandler,
)
})
}
@@ -143,6 +147,7 @@ func newProvider(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
systemDashboardHandler systemdashboard.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -172,6 +177,7 @@ func newProvider(
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
systemDashboardHandler: systemDashboardHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -272,6 +278,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addSystemDashboardRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,54 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/gorilla/mux"
)
func (provider *provider) addSystemDashboardRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/system/{source}/dashboard", handler.New(
provider.authZ.ViewAccess(provider.systemDashboardHandler.Get),
handler.OpenAPIDef{
ID: "GetSystemDashboard",
Tags: []string{"system-dashboard"},
Summary: "Get a system dashboard",
Description: "Returns the system dashboard for the given source. Seeds default panels on the first call if no dashboard exists yet.",
Request: nil,
RequestContentType: "",
Response: new(systemdashboardtypes.GettableSystemDashboard),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/system/{source}/dashboard", handler.New(
provider.authZ.EditAccess(provider.systemDashboardHandler.Update),
handler.OpenAPIDef{
ID: "UpdateSystemDashboard",
Tags: []string{"system-dashboard"},
Summary: "Update a system dashboard",
Description: "Replaces the panels, layout, and variables of a system dashboard.",
Request: new(systemdashboardtypes.UpdatableSystemDashboard),
RequestContentType: "application/json",
Response: new(systemdashboardtypes.GettableSystemDashboard),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
},
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -63,6 +63,7 @@ type RetryConfig struct {
func newConfig() factory.Config {
return Config{
Provider: "noop",
BufferSize: 1000,
BatchSize: 100,
FlushInterval: time.Second,

View File

@@ -0,0 +1,103 @@
package implsystemdashboard
import (
"context"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
type handler struct {
module systemdashboard.Module
providerSettings factory.ProviderSettings
}
func NewHandler(module systemdashboard.Module, providerSettings factory.ProviderSettings) systemdashboard.Handler {
return &handler{module: module, providerSettings: providerSettings}
}
func (h *handler) Get(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
}
source, err := sourceFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := h.module.Get(ctx, orgID, source)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, systemdashboardtypes.NewGettableSystemDashboard(dashboard))
}
func (h *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
}
source, err := sourceFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
req := new(systemdashboardtypes.UpdatableSystemDashboard)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
dashboard, err := h.module.Update(ctx, orgID, source, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, systemdashboardtypes.NewGettableSystemDashboard(dashboard))
}
// sourceFromPath extracts the {source} path variable.
func sourceFromPath(r *http.Request) (string, error) {
source := mux.Vars(r)["source"]
if source == "" {
return "", errors.Newf(errors.TypeInvalidInput, systemdashboardtypes.ErrCodeSystemDashboardInvalidInput, "source is missing from the path")
}
return source, nil
}

View File

@@ -0,0 +1,24 @@
package systemdashboard
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/systemdashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// System dashboards are pre-seeded, org-scoped dashboards identified by a source
type Module interface {
// Get returns the system dashboard for the given org and source,
// seeding default panels if the dashboard does not yet exist.
Get(ctx context.Context, orgID valuer.UUID, source string) (*systemdashboardtypes.SystemDashboard, error)
// Update replaces the data blob of an existing system dashboard.
Update(ctx context.Context, orgID valuer.UUID, source string, updatedBy string, req *systemdashboardtypes.UpdatableSystemDashboard) (*systemdashboardtypes.SystemDashboard, error)
}
type Handler interface {
Get(rw http.ResponseWriter, r *http.Request)
Update(rw http.ResponseWriter, r *http.Request)
}

View File

@@ -208,7 +208,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, s.signoz.Auditor).Wrap)
r.Use(middleware.NewComment().Wrap)
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz)

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/emailing"
@@ -123,6 +124,9 @@ type Config struct {
// ServiceAccount config
ServiceAccount serviceaccount.Config `mapstructure:"serviceaccount"`
// Auditor config
Auditor auditor.Config `mapstructure:"auditor"`
}
func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.ResolverConfig) (Config, error) {
@@ -153,6 +157,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
user.NewConfigFactory(),
identn.NewConfigFactory(),
serviceaccount.NewConfigFactory(),
auditor.NewConfigFactory(),
}
conf, err := config.New(ctx, resolverConfig, configFactories)

View File

@@ -26,6 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory/implrulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
@@ -62,6 +63,7 @@ type Handlers struct {
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
SystemDashboardHandler systemdashboard.Handler
}
func NewHandlers(

View File

@@ -27,6 +27,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/systemdashboard"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
@@ -69,6 +70,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ systemdashboard.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -3,6 +3,8 @@ package signoz
import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/noopauditor"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
@@ -286,6 +288,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RegistryHandler,
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.SystemDashboardHandler,
),
)
}
@@ -312,6 +315,12 @@ func NewGlobalProviderFactories(identNConfig identn.Config) factory.NamedMap[fac
)
}
func NewAuditorProviderFactories() factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
return factory.MustNewNamedMap(
noopauditor.NewFactory(),
)
}
func NewFlaggerProviderFactories(registry featuretypes.Registry) factory.NamedMap[factory.ProviderFactory[flagger.FlaggerProvider, flagger.Config]] {
return factory.MustNewNamedMap(
configflagger.NewFactory(registry),

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/sqlroutingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/apiserver"
@@ -75,6 +76,7 @@ type SigNoz struct {
QueryParser queryparser.QueryParser
Flagger flagger.Flagger
Gateway gateway.Gateway
Auditor auditor.Auditor
}
func New(
@@ -94,6 +96,7 @@ func New(
authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error),
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
auditorProviderFactories func(licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]],
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
) (*SigNoz, error) {
// Initialize instrumentation
@@ -371,6 +374,12 @@ func New(
return nil, err
}
// Initialize auditor from the variant-specific provider factories
auditor, err := factory.NewProviderFromNamedMap(ctx, providerSettings, config.Auditor, auditorProviderFactories(licensing), config.Auditor.Provider)
if err != nil {
return nil, err
}
// Initialize authns
store := sqlauthnstore.NewStore(sqlstore)
authNs, err := authNsCallback(ctx, providerSettings, store, licensing)
@@ -470,6 +479,7 @@ func New(
factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer),
factory.NewNamedService(factory.MustNewName("authz"), authz),
factory.NewNamedService(factory.MustNewName("user"), userService, factory.MustNewName("authz")),
factory.NewNamedService(factory.MustNewName("auditor"), auditor),
)
if err != nil {
return nil, err
@@ -516,5 +526,6 @@ func New(
QueryParser: queryParser,
Flagger: flagger,
Gateway: gateway,
Auditor: auditor,
}, nil
}

View File

@@ -62,6 +62,10 @@ type GettableServicesMetadata struct {
Services []*ServiceMetadata `json:"services" required:"true" nullable:"false"`
}
type ListServicesMetadataParams struct {
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
}
// Service represents a cloud integration service with its definition,
// cloud integration service is non nil only when the service entry exists in DB with ANY config (enabled or disabled).
type Service struct {
@@ -69,6 +73,10 @@ type Service struct {
CloudIntegrationService *CloudIntegrationService `json:"cloudIntegrationService" required:"true" nullable:"true"`
}
type GetServiceParams struct {
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
}
type UpdatableService struct {
Config *ServiceConfig `json:"config" required:"true" nullable:"false"`
}

View File

@@ -0,0 +1,113 @@
package systemdashboardtypes
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// Data is the panel + layout + variables blob for a system dashboard.
// It uses the same format as dashboard.data so the frontend can render it
// with the existing dashboard component. Stored as a JSON text column.
type Data map[string]any
func (d Data) Value() (driver.Value, error) {
if d == nil {
return "{}", nil
}
b, err := json.Marshal(d)
if err != nil {
return nil, err
}
return string(b), nil
}
func (d *Data) Scan(src any) error {
var raw []byte
switch v := src.(type) {
case string:
raw = []byte(v)
case []byte:
raw = v
case nil:
*d = Data{}
return nil
default:
return fmt.Errorf("systemdashboardtypes: cannot scan %T into Data", src)
}
return json.Unmarshal(raw, d)
}
// StorableSystemDashboard is the bun/DB representation of a system dashboard.
// Each (org_id, source) pair is unique — the source identifies which pre-seeded
// page the dashboard belongs to (e.g. "ai-o11y-overview", "service-overview").
type StorableSystemDashboard struct {
bun.BaseModel `bun:"table:system_dashboard,alias:system_dashboard"`
types.Identifiable
types.TimeAuditable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
Source string `bun:"source,type:text,notnull"`
Data Data `bun:"data,type:text,notnull"`
}
// SystemDashboard is the domain model for a system dashboard.
type SystemDashboard struct {
types.TimeAuditable
ID string
OrgID valuer.UUID
Source string
Data Data
}
// NewSystemDashboardFromStorable converts a StorableSystemDashboard to a SystemDashboard.
func NewSystemDashboardFromStorable(s *StorableSystemDashboard) *SystemDashboard {
data := make(Data, len(s.Data))
for k, v := range s.Data {
data[k] = v
}
return &SystemDashboard{
TimeAuditable: s.TimeAuditable,
ID: s.ID.StringValue(),
OrgID: s.OrgID,
Source: s.Source,
Data: data,
}
}
// GettableSystemDashboard is the HTTP response representation of a system dashboard.
type GettableSystemDashboard struct {
ID string `json:"id" required:"true"`
Source string `json:"source" required:"true"`
Data Data `json:"data" required:"true"`
CreatedAt time.Time `json:"created_at" required:"true"`
UpdatedAt time.Time `json:"updated_at" required:"true"`
}
// NewGettableSystemDashboard converts a domain SystemDashboard to a GettableSystemDashboard.
func NewGettableSystemDashboard(d *SystemDashboard) *GettableSystemDashboard {
data := make(Data, len(d.Data))
for k, v := range d.Data {
data[k] = v
}
return &GettableSystemDashboard{
ID: d.ID,
Source: d.Source,
Data: data,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
}
}
// UpdatableSystemDashboard is the HTTP request body for updating a system dashboard.
// The entire data blob is replaced on PUT.
type UpdatableSystemDashboard struct {
Data Data `json:"data" required:"true"`
}

View File

@@ -0,0 +1,9 @@
package systemdashboardtypes
import "github.com/SigNoz/signoz/pkg/errors"
var (
ErrCodeSystemDashboardNotFound = errors.MustNewCode("system_dashboard_not_found")
ErrCodeSystemDashboardInvalidInput = errors.MustNewCode("system_dashboard_invalid_input")
ErrCodeSystemDashboardInvalidData = errors.MustNewCode("system_dashboard_invalid_data")
)

View File

@@ -0,0 +1,13 @@
package systemdashboardtypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
Get(ctx context.Context, orgID valuer.UUID, source string) (*StorableSystemDashboard, error)
Create(ctx context.Context, dashboard *StorableSystemDashboard) error
Update(ctx context.Context, dashboard *StorableSystemDashboard) error
}