Compare commits

...

12 Commits

Author SHA1 Message Date
Nikhil Soni
18d71bb472 fix: switch to using http_host instead of external_http_url
external_http_url stores the hostname but the name
is confusing, so switching to http_host
2026-02-06 13:42:14 +05:30
Nikhil Soni
4d9282ad18 fix: fix incosistent use of http attribute in ext. api
HTTP attributes like http.url, url.full along with server.name and net.peer.name
were used inconsitantly leading to bugs in aggregation query and they were
expensive to query as well since these attr are stored as json instead of
direct columns. Using columns like http_url optimises these queries since
it gets populated using all relevant attributes during ingestion itself.
2026-02-06 13:42:14 +05:30
Ashwin Bhatkal
66f4c3d6ec chore: add max-params eslint rule and update contribution guidelines (#10200)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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: add max-params eslint rule and update contribution guidelines

* chore: add no-cycle rule
2026-02-06 13:03:59 +05:30
Srikanth Chekuri
a0f407a848 chore(metricsexplorer): update tags and regenerate (#10197) 2026-02-06 12:10:11 +05:30
Abhi kumar
3562de8fbb fix: added fix for tooltip sizing (#10205)
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
2026-02-05 22:46:17 +05:30
Abhi kumar
ef80fb39fd feat: added new time-series graph (#10201)
* feat: added new time-series graph

* chore: updated types for charts
2026-02-05 11:29:21 +00:00
Abhi kumar
594d4dc737 feat: added changes to compute legend items width for virtualization (#10196)
* feat: added changes to compute legend items width for virtualization

* feat: added support for single row in legends
2026-02-05 16:27:43 +05:30
Abhishek Kumar Singh
01415b58be chore: made group_wait and group_interval configuration dynamic for alert manager (#10198) 2026-02-05 09:56:27 +00:00
Ashwin Bhatkal
f7728c9019 chore: update variables store with derived values (#10194)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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: remove redundant sort

* chore: use sorted variables array

* chore: update variables store with derived values

* chore: fix types

* chore: resolve cursor comments
2026-02-05 14:09:46 +05:30
Amlan Kumar Nandy
3fffe6e198 chore: use single reducer in create alerts flow (#10179) 2026-02-05 07:39:07 +00:00
Aditya Singh
4594f4ffe3 Fix/query label fix (#10158)
* feat: function idon func added

* feat: test update

* fix: minor refactor
2026-02-05 12:19:16 +05:30
Abhi kumar
b660dc8573 fix: added fix for storing legend preferences (#10184)
* fix: added fix for storing legend preferences

* fix: fixed tsc

* fix: cursor reviews

* fix: cursor reviews

* fix: added fix for legend visibility issue

* fix: pr review changes
2026-02-05 12:05:09 +05:30
63 changed files with 1608 additions and 794 deletions

5
.gitignore vendored
View File

@@ -1,8 +1,11 @@
node_modules
# editor
.vscode
!.vscode/settings.json
.zed
.idea
deploy/docker/environment_tiny/common_test
frontend/node_modules
@@ -31,8 +34,6 @@ frontend/yarn-debug.log*
frontend/yarn-error.log*
frontend/src/constants/env.ts
.idea
**/build
**/storage
**/locust-scripts/__pycache__/

View File

@@ -2665,6 +2665,7 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2719,6 +2720,7 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2774,6 +2776,7 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2940,6 +2943,7 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -3807,6 +3811,9 @@ components:
type: string
alertName:
type: string
required:
- alertName
- alertId
type: object
MetricsexplorertypesMetricAlertsResponse:
properties:
@@ -3815,6 +3822,8 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesMetricAlert'
nullable: true
type: array
required:
- alerts
type: object
MetricsexplorertypesMetricAttribute:
properties:
@@ -3828,6 +3837,10 @@ components:
type: string
nullable: true
type: array
required:
- key
- values
- valueCount
type: object
MetricsexplorertypesMetricAttributesRequest:
properties:
@@ -3839,6 +3852,8 @@ components:
start:
nullable: true
type: integer
required:
- metricName
type: object
MetricsexplorertypesMetricAttributesResponse:
properties:
@@ -3850,6 +3865,9 @@ components:
totalKeys:
format: int64
type: integer
required:
- attributes
- totalKeys
type: object
MetricsexplorertypesMetricDashboard:
properties:
@@ -3861,6 +3879,11 @@ components:
type: string
widgetName:
type: string
required:
- dashboardName
- dashboardId
- widgetId
- widgetName
type: object
MetricsexplorertypesMetricDashboardsResponse:
properties:
@@ -3869,6 +3892,8 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
nullable: true
type: array
required:
- dashboards
type: object
MetricsexplorertypesMetricHighlightsResponse:
properties:
@@ -3884,6 +3909,11 @@ components:
totalTimeSeries:
minimum: 0
type: integer
required:
- dataPoints
- lastReceived
- totalTimeSeries
- activeTimeSeries
type: object
MetricsexplorertypesMetricMetadata:
properties:
@@ -3892,11 +3922,27 @@ components:
isMonotonic:
type: boolean
temporality:
enum:
- delta
- cumulative
- unspecified
type: string
type:
enum:
- gauge
- sum
- histogram
- summary
- exponentialhistogram
type: string
unit:
type: string
required:
- description
- type
- unit
- temporality
- isMonotonic
type: object
MetricsexplorertypesStat:
properties:
@@ -3911,9 +3957,22 @@ components:
minimum: 0
type: integer
type:
enum:
- gauge
- sum
- histogram
- summary
- exponentialhistogram
type: string
unit:
type: string
required:
- metricName
- description
- type
- unit
- timeseries
- samples
type: object
MetricsexplorertypesStatsRequest:
properties:
@@ -3931,6 +3990,10 @@ components:
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
MetricsexplorertypesStatsResponse:
properties:
@@ -3942,6 +4005,9 @@ components:
total:
minimum: 0
type: integer
required:
- metrics
- total
type: object
MetricsexplorertypesTreemapEntry:
properties:
@@ -3953,6 +4019,10 @@ components:
totalValue:
minimum: 0
type: integer
required:
- metricName
- percentage
- totalValue
type: object
MetricsexplorertypesTreemapRequest:
properties:
@@ -3964,10 +4034,18 @@ components:
limit:
type: integer
mode:
enum:
- timeseries
- samples
type: string
start:
format: int64
type: integer
required:
- start
- end
- limit
- mode
type: object
MetricsexplorertypesTreemapResponse:
properties:
@@ -3981,6 +4059,9 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesTreemapEntry'
nullable: true
type: array
required:
- timeseries
- samples
type: object
MetricsexplorertypesUpdateMetricMetadataRequest:
properties:
@@ -3991,11 +4072,28 @@ components:
metricName:
type: string
temporality:
enum:
- delta
- cumulative
- unspecified
type: string
type:
enum:
- gauge
- sum
- histogram
- summary
- exponentialhistogram
type: string
unit:
type: string
required:
- metricName
- type
- description
- unit
- temporality
- isMonotonic
type: object
PreferencetypesPreference:
properties:

View File

@@ -61,6 +61,8 @@ module.exports = {
curly: 'error', // Requires curly braces for all control statements
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
// TODO: Change this to error in May 2026
'max-params': ['warn', 3], // a function can have max 3 params after which it should become an object
// TypeScript rules
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
@@ -116,7 +118,7 @@ module.exports = {
},
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }], // Prevents importing packages not in package.json
// 'import/no-cycle': 'warn', // TODO: Enable later to detect circular dependencies
'import/no-cycle': 'warn', // Warns about circular dependencies
// Import sorting rules
'simple-import-sort/imports': [
@@ -146,6 +148,19 @@ module.exports = {
'sonarjs/no-duplicate-string': 'off', // Disabled - can be noisy (enable periodically to check)
},
overrides: [
{
files: [
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/__tests__/**/*.{js,jsx,ts,tsx}',
],
rules: {
// Tests often have intentional duplication and complexity - disable SonarJS rules
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
},
},
{
files: ['src/api/generated/**/*.ts'],
rules: {
@@ -153,7 +168,6 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-nested-ternary': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'sonarjs/no-duplicate-string': 'off',
},
},
],

View File

@@ -2,6 +2,11 @@
Embrace the spirit of collaboration and contribute to the success of our open-source project by adhering to these frontend development guidelines with precision and passion.
### Export Style
- **React components** (`src/components/`, `src/container/`, `src/pages/`): Prefer **default exports** for the main component in each file
- **Utilities, hooks, APIs, types, constants** (`src/utils/`, `src/hooks/`, `src/api/`, `src/lib/`, `src/types/`, `src/constants/`): Prefer **named exports** for better tree-shaking and explicit imports
### React and Components
- Strive to create small and modular components, ensuring they are divided into individual pieces for improved maintainability and reusability.

View File

@@ -28,8 +28,10 @@ import type {
GatewaytypesPostableIngestionKeyLimitDTO,
GatewaytypesUpdatableIngestionKeyLimitDTO,
GetIngestionKeys200,
GetIngestionKeysParams,
RenderErrorResponseDTO,
SearchIngestionKeys200,
SearchIngestionKeysParams,
UpdateIngestionKeyLimitPathParameters,
UpdateIngestionKeyPathParameters,
} from '../sigNoz.schemas';
@@ -42,35 +44,44 @@ type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
* This endpoint returns the ingestion keys for a workspace
* @summary Get ingestion keys for workspace
*/
export const getIngestionKeys = (signal?: AbortSignal) => {
export const getIngestionKeys = (
params?: GetIngestionKeysParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetIngestionKeys200>({
url: `/api/v2/gateway/ingestion_keys`,
method: 'GET',
params,
signal,
});
};
export const getGetIngestionKeysQueryKey = () => {
return ['getIngestionKeys'] as const;
export const getGetIngestionKeysQueryKey = (
params?: GetIngestionKeysParams,
) => {
return ['getIngestionKeys', ...(params ? [params] : [])] as const;
};
export const getGetIngestionKeysQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionKeys>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
}) => {
>(
params?: GetIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetIngestionKeysQueryKey();
const queryKey = queryOptions?.queryKey ?? getGetIngestionKeysQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getIngestionKeys>>> = ({
signal,
}) => getIngestionKeys(signal);
}) => getIngestionKeys(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
@@ -91,14 +102,17 @@ export type GetIngestionKeysQueryError = RenderErrorResponseDTO;
export function useGetIngestionKeys<
TData = Awaited<ReturnType<typeof getIngestionKeys>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeysQueryOptions(options);
>(
params?: GetIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeysQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -114,10 +128,11 @@ export function useGetIngestionKeys<
*/
export const invalidateGetIngestionKeys = async (
queryClient: QueryClient,
params?: GetIngestionKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionKeysQueryKey() },
{ queryKey: getGetIngestionKeysQueryKey(params) },
options,
);
@@ -662,35 +677,45 @@ export const useUpdateIngestionKeyLimit = <
* This endpoint returns the ingestion keys for a workspace
* @summary Search ingestion keys for workspace
*/
export const searchIngestionKeys = (signal?: AbortSignal) => {
export const searchIngestionKeys = (
params?: SearchIngestionKeysParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<SearchIngestionKeys200>({
url: `/api/v2/gateway/ingestion_keys/search`,
method: 'GET',
params,
signal,
});
};
export const getSearchIngestionKeysQueryKey = () => {
return ['searchIngestionKeys'] as const;
export const getSearchIngestionKeysQueryKey = (
params?: SearchIngestionKeysParams,
) => {
return ['searchIngestionKeys', ...(params ? [params] : [])] as const;
};
export const getSearchIngestionKeysQueryOptions = <
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
}) => {
>(
params?: SearchIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getSearchIngestionKeysQueryKey();
const queryKey =
queryOptions?.queryKey ?? getSearchIngestionKeysQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof searchIngestionKeys>>
> = ({ signal }) => searchIngestionKeys(signal);
> = ({ signal }) => searchIngestionKeys(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
@@ -711,14 +736,17 @@ export type SearchIngestionKeysQueryError = RenderErrorResponseDTO;
export function useSearchIngestionKeys<
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
TError = RenderErrorResponseDTO
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getSearchIngestionKeysQueryOptions(options);
>(
params?: SearchIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getSearchIngestionKeysQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -734,10 +762,11 @@ export function useSearchIngestionKeys<
*/
export const invalidateSearchIngestionKeys = async (
queryClient: QueryClient,
params?: SearchIngestionKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getSearchIngestionKeysQueryKey() },
{ queryKey: getSearchIngestionKeysQueryKey(params) },
options,
);

View File

@@ -47,7 +47,7 @@ type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
* @summary Get metric alerts
*/
export const getMetricAlerts = (
params?: GetMetricAlertsParams,
params: GetMetricAlertsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAlerts200>({
@@ -66,7 +66,7 @@ export const getGetMetricAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricAlertsParams,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -103,7 +103,7 @@ export function useGetMetricAlerts<
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricAlertsParams,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -128,7 +128,7 @@ export function useGetMetricAlerts<
*/
export const invalidateGetMetricAlerts = async (
queryClient: QueryClient,
params?: GetMetricAlertsParams,
params: GetMetricAlertsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
@@ -144,7 +144,7 @@ export const invalidateGetMetricAlerts = async (
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
params?: GetMetricDashboardsParams,
params: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
@@ -165,7 +165,7 @@ export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricDashboardsParams,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -203,7 +203,7 @@ export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricDashboardsParams,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -228,7 +228,7 @@ export function useGetMetricDashboards<
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
params?: GetMetricDashboardsParams,
params: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
@@ -244,7 +244,7 @@ export const invalidateGetMetricDashboards = async (
* @summary Get metric highlights
*/
export const getMetricHighlights = (
params?: GetMetricHighlightsParams,
params: GetMetricHighlightsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricHighlights200>({
@@ -265,7 +265,7 @@ export const getGetMetricHighlightsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricHighlightsParams,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -303,7 +303,7 @@ export function useGetMetricHighlights<
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricHighlightsParams,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -328,7 +328,7 @@ export function useGetMetricHighlights<
*/
export const invalidateGetMetricHighlights = async (
queryClient: QueryClient,
params?: GetMetricHighlightsParams,
params: GetMetricHighlightsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
@@ -526,7 +526,7 @@ export const useGetMetricAttributes = <
* @summary Get metric metadata
*/
export const getMetricMetadata = (
params?: GetMetricMetadataParams,
params: GetMetricMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
@@ -547,7 +547,7 @@ export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricMetadataParams,
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
@@ -585,7 +585,7 @@ export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
params?: GetMetricMetadataParams,
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
@@ -610,7 +610,7 @@ export function useGetMetricMetadata<
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
params?: GetMetricMetadataParams,
params: GetMetricMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(

View File

@@ -650,11 +650,11 @@ export interface MetricsexplorertypesMetricAlertDTO {
/**
* @type string
*/
alertId?: string;
alertId: string;
/**
* @type string
*/
alertName?: string;
alertName: string;
}
export interface MetricsexplorertypesMetricAlertsResponseDTO {
@@ -662,24 +662,24 @@ export interface MetricsexplorertypesMetricAlertsResponseDTO {
* @type array
* @nullable true
*/
alerts?: MetricsexplorertypesMetricAlertDTO[] | null;
alerts: MetricsexplorertypesMetricAlertDTO[] | null;
}
export interface MetricsexplorertypesMetricAttributeDTO {
/**
* @type string
*/
key?: string;
key: string;
/**
* @type integer
* @minimum 0
*/
valueCount?: number;
valueCount: number;
/**
* @type array
* @nullable true
*/
values?: string[] | null;
values: string[] | null;
}
export interface MetricsexplorertypesMetricAttributesRequestDTO {
@@ -691,7 +691,7 @@ export interface MetricsexplorertypesMetricAttributesRequestDTO {
/**
* @type string
*/
metricName?: string;
metricName: string;
/**
* @type integer
* @nullable true
@@ -704,31 +704,31 @@ export interface MetricsexplorertypesMetricAttributesResponseDTO {
* @type array
* @nullable true
*/
attributes?: MetricsexplorertypesMetricAttributeDTO[] | null;
attributes: MetricsexplorertypesMetricAttributeDTO[] | null;
/**
* @type integer
* @format int64
*/
totalKeys?: number;
totalKeys: number;
}
export interface MetricsexplorertypesMetricDashboardDTO {
/**
* @type string
*/
dashboardId?: string;
dashboardId: string;
/**
* @type string
*/
dashboardName?: string;
dashboardName: string;
/**
* @type string
*/
widgetId?: string;
widgetId: string;
/**
* @type string
*/
widgetName?: string;
widgetName: string;
}
export interface MetricsexplorertypesMetricDashboardsResponseDTO {
@@ -736,7 +736,7 @@ export interface MetricsexplorertypesMetricDashboardsResponseDTO {
* @type array
* @nullable true
*/
dashboards?: MetricsexplorertypesMetricDashboardDTO[] | null;
dashboards: MetricsexplorertypesMetricDashboardDTO[] | null;
}
export interface MetricsexplorertypesMetricHighlightsResponseDTO {
@@ -744,74 +744,96 @@ export interface MetricsexplorertypesMetricHighlightsResponseDTO {
* @type integer
* @minimum 0
*/
activeTimeSeries?: number;
activeTimeSeries: number;
/**
* @type integer
* @minimum 0
*/
dataPoints?: number;
dataPoints: number;
/**
* @type integer
* @minimum 0
*/
lastReceived?: number;
lastReceived: number;
/**
* @type integer
* @minimum 0
*/
totalTimeSeries?: number;
totalTimeSeries: number;
}
export enum MetricsexplorertypesMetricMetadataDTOTemporality {
delta = 'delta',
cumulative = 'cumulative',
unspecified = 'unspecified',
}
export enum MetricsexplorertypesMetricMetadataDTOType {
gauge = 'gauge',
sum = 'sum',
histogram = 'histogram',
summary = 'summary',
exponentialhistogram = 'exponentialhistogram',
}
export interface MetricsexplorertypesMetricMetadataDTO {
/**
* @type string
*/
description?: string;
description: string;
/**
* @type boolean
*/
isMonotonic?: boolean;
isMonotonic: boolean;
/**
* @enum delta,cumulative,unspecified
* @type string
*/
temporality: MetricsexplorertypesMetricMetadataDTOTemporality;
/**
* @enum gauge,sum,histogram,summary,exponentialhistogram
* @type string
*/
type: MetricsexplorertypesMetricMetadataDTOType;
/**
* @type string
*/
temporality?: string;
/**
* @type string
*/
type?: string;
/**
* @type string
*/
unit?: string;
unit: string;
}
export enum MetricsexplorertypesStatDTOType {
gauge = 'gauge',
sum = 'sum',
histogram = 'histogram',
summary = 'summary',
exponentialhistogram = 'exponentialhistogram',
}
export interface MetricsexplorertypesStatDTO {
/**
* @type string
*/
description?: string;
description: string;
/**
* @type string
*/
metricName?: string;
metricName: string;
/**
* @type integer
* @minimum 0
*/
samples?: number;
samples: number;
/**
* @type integer
* @minimum 0
*/
timeseries?: number;
timeseries: number;
/**
* @enum gauge,sum,histogram,summary,exponentialhistogram
* @type string
*/
type: MetricsexplorertypesStatDTOType;
/**
* @type string
*/
type?: string;
/**
* @type string
*/
unit?: string;
unit: string;
}
export interface MetricsexplorertypesStatsRequestDTO {
@@ -819,12 +841,12 @@ export interface MetricsexplorertypesStatsRequestDTO {
* @type integer
* @format int64
*/
end?: number;
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type integer
*/
limit?: number;
limit: number;
/**
* @type integer
*/
@@ -834,7 +856,7 @@ export interface MetricsexplorertypesStatsRequestDTO {
* @type integer
* @format int64
*/
start?: number;
start: number;
}
export interface MetricsexplorertypesStatsResponseDTO {
@@ -842,51 +864,56 @@ export interface MetricsexplorertypesStatsResponseDTO {
* @type array
* @nullable true
*/
metrics?: MetricsexplorertypesStatDTO[] | null;
metrics: MetricsexplorertypesStatDTO[] | null;
/**
* @type integer
* @minimum 0
*/
total?: number;
total: number;
}
export interface MetricsexplorertypesTreemapEntryDTO {
/**
* @type string
*/
metricName?: string;
metricName: string;
/**
* @type number
* @format double
*/
percentage?: number;
percentage: number;
/**
* @type integer
* @minimum 0
*/
totalValue?: number;
totalValue: number;
}
export enum MetricsexplorertypesTreemapRequestDTOMode {
timeseries = 'timeseries',
samples = 'samples',
}
export interface MetricsexplorertypesTreemapRequestDTO {
/**
* @type integer
* @format int64
*/
end?: number;
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type integer
*/
limit?: number;
limit: number;
/**
* @enum timeseries,samples
* @type string
*/
mode?: string;
mode: MetricsexplorertypesTreemapRequestDTOMode;
/**
* @type integer
* @format int64
*/
start?: number;
start: number;
}
export interface MetricsexplorertypesTreemapResponseDTO {
@@ -894,39 +921,53 @@ export interface MetricsexplorertypesTreemapResponseDTO {
* @type array
* @nullable true
*/
samples?: MetricsexplorertypesTreemapEntryDTO[] | null;
samples: MetricsexplorertypesTreemapEntryDTO[] | null;
/**
* @type array
* @nullable true
*/
timeseries?: MetricsexplorertypesTreemapEntryDTO[] | null;
timeseries: MetricsexplorertypesTreemapEntryDTO[] | null;
}
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality {
delta = 'delta',
cumulative = 'cumulative',
unspecified = 'unspecified',
}
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOType {
gauge = 'gauge',
sum = 'sum',
histogram = 'histogram',
summary = 'summary',
exponentialhistogram = 'exponentialhistogram',
}
export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
/**
* @type string
*/
description?: string;
description: string;
/**
* @type boolean
*/
isMonotonic?: boolean;
isMonotonic: boolean;
/**
* @type string
*/
metricName?: string;
metricName: string;
/**
* @enum delta,cumulative,unspecified
* @type string
*/
temporality: MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality;
/**
* @enum gauge,sum,histogram,summary,exponentialhistogram
* @type string
*/
type: MetricsexplorertypesUpdateMetricMetadataRequestDTOType;
/**
* @type string
*/
temporality?: string;
/**
* @type string
*/
type?: string;
/**
* @type string
*/
unit?: string;
unit: string;
}
export interface PreferencetypesPreferenceDTO {
@@ -1851,6 +1892,19 @@ export type GetFeatures200 = {
status?: string;
};
export type GetIngestionKeysParams = {
/**
* @type integer
* @description undefined
*/
page?: number;
/**
* @type integer
* @description undefined
*/
per_page?: number;
};
export type GetIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
/**
@@ -1890,6 +1944,24 @@ export type DeleteIngestionKeyLimitPathParameters = {
export type UpdateIngestionKeyLimitPathParameters = {
limitId: string;
};
export type SearchIngestionKeysParams = {
/**
* @type string
* @description undefined
*/
name?: string;
/**
* @type integer
* @description undefined
*/
page?: number;
/**
* @type integer
* @description undefined
*/
per_page?: number;
};
export type SearchIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
/**
@@ -1903,7 +1975,7 @@ export type GetMetricAlertsParams = {
* @type string
* @description undefined
*/
metricName?: string;
metricName: string;
};
export type GetMetricAlerts200 = {
@@ -1919,7 +1991,7 @@ export type GetMetricDashboardsParams = {
* @type string
* @description undefined
*/
metricName?: string;
metricName: string;
};
export type GetMetricDashboards200 = {
@@ -1935,7 +2007,7 @@ export type GetMetricHighlightsParams = {
* @type string
* @description undefined
*/
metricName?: string;
metricName: string;
};
export type GetMetricHighlights200 = {
@@ -1962,7 +2034,7 @@ export type GetMetricMetadataParams = {
* @type string
* @description undefined
*/
metricName?: string;
metricName: string;
};
export type GetMetricMetadata200 = {

View File

@@ -11,9 +11,14 @@ import {
IBuilderQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { UseQueryOperations } from 'types/common/operations.types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import {
DataSource,
QueryBuilderContextType,
QueryFunctionsTypes,
} from 'types/common/queryBuilder';
import '@testing-library/jest-dom';
@@ -45,12 +50,17 @@ const mockedUseQueryOperations = jest.mocked(
describe('QueryBuilderV2 + QueryV2 - base render', () => {
let handleRunQueryMock: jest.MockedFunction<() => void>;
let handleQueryFunctionsUpdatesMock: jest.MockedFunction<() => void>;
let baseQBContext: QueryBuilderContextType;
beforeEach(() => {
const mockCloneQuery = jest.fn() as jest.MockedFunction<
(type: string, q: IBuilderQuery) => void
>;
handleRunQueryMock = jest.fn() as jest.MockedFunction<() => void>;
handleQueryFunctionsUpdatesMock = jest.fn() as jest.MockedFunction<
() => void
>;
const baseQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.LOGS,
@@ -91,7 +101,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
const updateQueriesData: QueryBuilderContextType['updateQueriesData'] = (q) =>
q;
mockedUseQueryBuilder.mockReturnValue(({
const baseContext = ({
currentQuery: currentQueryObj,
stagedQuery: null,
lastUsedQuery: null,
@@ -124,7 +134,10 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
} as unknown) as QueryBuilderContextType);
} as unknown) as QueryBuilderContextType;
baseQBContext = baseContext;
mockedUseQueryBuilder.mockReturnValue(baseQBContext);
mockedUseQueryOperations.mockReturnValue({
isTracePanelType: false,
@@ -139,7 +152,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
handleDeleteQuery: jest.fn(),
handleChangeQueryData: (jest.fn() as unknown) as ReturnType<UseQueryOperations>['handleChangeQueryData'],
handleChangeFormulaData: jest.fn(),
handleQueryFunctionsUpdates: jest.fn(),
handleQueryFunctionsUpdates: handleQueryFunctionsUpdatesMock,
listOfAdditionalFormulaFilters: [],
});
});
@@ -199,4 +212,56 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
expect(handleRunQueryMock).toHaveBeenCalled();
});
it('fx button is disabled when functions already exist', () => {
const currentQueryBase = baseQBContext.currentQuery as Query;
const supersetQueryBase = baseQBContext.supersetQuery as Query;
mockedUseQueryBuilder.mockReturnValueOnce({
...baseQBContext,
currentQuery: {
...currentQueryBase,
builder: {
...currentQueryBase.builder,
queryData: [
{
...currentQueryBase.builder.queryData[0],
functions: [
{ name: QueryFunctionsTypes.TIME_SHIFT, args: [] } as QueryFunction,
],
},
],
},
},
supersetQuery: {
...supersetQueryBase,
builder: {
...supersetQueryBase.builder,
queryData: [
{
...supersetQueryBase.builder.queryData[0],
functions: [
{ name: QueryFunctionsTypes.TIME_SHIFT, args: [] } as QueryFunction,
],
},
],
},
},
});
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />);
const fxButton = document.querySelector('.function-btn') as HTMLButtonElement;
expect(fxButton).toBeInTheDocument();
expect(fxButton).toBeDisabled();
const deleteButton = document.querySelector(
'.query-function-delete-btn',
) as HTMLButtonElement;
expect(deleteButton).toBeInTheDocument();
userEvent.click(deleteButton);
waitFor(() => {
expect(fxButton).not.toBeDisabled();
});
});
});

View File

@@ -14,6 +14,6 @@ export const VIEW_TYPES = {
export const SPAN_ATTRIBUTES = {
URL_PATH: 'http.url',
RESPONSE_STATUS_CODE: 'response_status_code',
SERVER_NAME: 'net.peer.name',
SERVER_NAME: 'http_host',
SERVER_PORT: 'net.peer.port',
} as const;

View File

@@ -638,7 +638,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: 'tag',
type: '',
},
op: '=',
value: domainName,
@@ -685,7 +685,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: 'tag',
type: '',
},
op: '=',
value: domainName,
@@ -733,7 +733,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: 'tag',
type: '',
},
op: '=',
value: domainName,
@@ -780,7 +780,7 @@ export const getEndPointsQueryPayload = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: 'tag',
type: '',
},
op: '=',
value: domainName,
@@ -1302,7 +1302,7 @@ export const getTopErrorsCoRelationQueryFilters = (
{
id: 'e8a043b7',
key: {
key: 'net.peer.name',
key: SPAN_ATTRIBUTES.SERVER_NAME,
dataType: DataTypes.String,
type: '',
},
@@ -2198,7 +2198,7 @@ export const getEndPointZeroStateQueryPayload = (
key: {
key: SPAN_ATTRIBUTES.SERVER_NAME,
dataType: DataTypes.String,
type: 'tag',
type: '',
},
op: '=',
value: domainName,
@@ -2793,7 +2793,7 @@ export const getStatusCodeBarChartWidgetData = (
key: {
dataType: DataTypes.String,
key: SPAN_ATTRIBUTES.SERVER_NAME,
type: 'tag',
type: '',
},
op: '=',
value: domainName,

View File

@@ -338,7 +338,7 @@ describe('CreateAlertV2 utils', () => {
const props = getCreateAlertLocalStateFromAlertDef(args);
expect(props).toBeDefined();
expect(props).toMatchObject({
basicAlertState: {
basic: {
...INITIAL_ALERT_STATE,
name: 'test-alert',
labels: {
@@ -348,10 +348,10 @@ describe('CreateAlertV2 utils', () => {
yAxisUnit: UniversalYAxisUnit.MINUTES,
},
// as we have already verified these utils in their respective tests
thresholdState: expect.any(Object),
advancedOptionsState: expect.any(Object),
evaluationWindowState: expect.any(Object),
notificationSettingsState: expect.any(Object),
threshold: expect.any(Object),
advancedOptions: expect.any(Object),
evaluationWindow: expect.any(Object),
notificationSettings: expect.any(Object),
});
});
});

View File

@@ -196,3 +196,11 @@ export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
description: NOTIFICATION_MESSAGE_PLACEHOLDER,
routingPolicies: false,
};
export const INITIAL_CREATE_ALERT_STATE = {
basic: INITIAL_ALERT_STATE,
threshold: INITIAL_ALERT_THRESHOLD_STATE,
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
};

View File

@@ -17,26 +17,22 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { INITIAL_CREATE_ALERT_STATE } from './constants';
import {
INITIAL_ADVANCED_OPTIONS_STATE,
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from './constants';
import {
AdvancedOptionsAction,
AlertThresholdAction,
AlertThresholdMatchType,
CreateAlertAction,
CreateAlertSlice,
EvaluationWindowAction,
ICreateAlertContextProps,
ICreateAlertProviderProps,
NotificationSettingsAction,
} from './types';
import {
advancedOptionsReducer,
alertCreationReducer,
alertThresholdReducer,
buildInitialAlertDef,
evaluationWindowReducer,
createAlertReducer,
getInitialAlertTypeFromURL,
notificationSettingsReducer,
} from './utils';
const CreateAlertContext = createContext<ICreateAlertContextProps | null>(null);
@@ -65,10 +61,65 @@ export function CreateAlertProvider(
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const [alertState, setAlertState] = useReducer(alertCreationReducer, {
...INITIAL_ALERT_STATE,
yAxisUnit: currentQuery.unit,
});
const [createAlertState, setCreateAlertState] = useReducer(
createAlertReducer,
{
...INITIAL_CREATE_ALERT_STATE,
basic: {
...INITIAL_CREATE_ALERT_STATE.basic,
yAxisUnit: currentQuery.unit,
},
},
);
const setAlertState = useCallback(
(action: CreateAlertAction) => {
setCreateAlertState({
slice: CreateAlertSlice.BASIC,
action,
});
},
[setCreateAlertState],
);
const setThresholdState = useCallback(
(action: AlertThresholdAction) => {
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action,
});
},
[setCreateAlertState],
);
const setEvaluationWindow = useCallback(
(action: EvaluationWindowAction) => {
setCreateAlertState({
slice: CreateAlertSlice.EVALUATION_WINDOW,
action,
});
},
[setCreateAlertState],
);
const setAdvancedOptions = useCallback(
(action: AdvancedOptionsAction) => {
setCreateAlertState({
slice: CreateAlertSlice.ADVANCED_OPTIONS,
action,
});
},
[setCreateAlertState],
);
const setNotificationSettings = useCallback(
(action: NotificationSettingsAction) => {
setCreateAlertState({
slice: CreateAlertSlice.NOTIFICATION_SETTINGS,
action,
});
},
[setCreateAlertState],
);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
@@ -104,92 +155,56 @@ export function CreateAlertProvider(
[redirectWithQueryBuilderData],
);
const [thresholdState, setThresholdState] = useReducer(
alertThresholdReducer,
INITIAL_ALERT_THRESHOLD_STATE,
);
const [evaluationWindow, setEvaluationWindow] = useReducer(
evaluationWindowReducer,
INITIAL_EVALUATION_WINDOW_STATE,
);
const [advancedOptions, setAdvancedOptions] = useReducer(
advancedOptionsReducer,
INITIAL_ADVANCED_OPTIONS_STATE,
);
const [notificationSettings, setNotificationSettings] = useReducer(
notificationSettingsReducer,
INITIAL_NOTIFICATION_SETTINGS_STATE,
);
useEffect(() => {
setThresholdState({
type: 'RESET',
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'RESET',
},
});
if (thresholdsFromURL) {
try {
const thresholds = JSON.parse(thresholdsFromURL);
setThresholdState({
type: 'SET_THRESHOLDS',
payload: thresholds,
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_THRESHOLDS',
payload: thresholds,
},
});
} catch (error) {
console.error('Error parsing thresholds from URL:', error);
}
setEvaluationWindow({
type: 'SET_INITIAL_STATE_FOR_METER',
setCreateAlertState({
slice: CreateAlertSlice.EVALUATION_WINDOW,
action: {
type: 'SET_INITIAL_STATE_FOR_METER',
},
});
setThresholdState({
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.IN_TOTAL,
setCreateAlertState({
slice: CreateAlertSlice.THRESHOLD,
action: {
type: 'SET_MATCH_TYPE',
payload: AlertThresholdMatchType.IN_TOTAL,
},
});
}
}, [alertType, thresholdsFromURL]);
useEffect(() => {
if (isEditMode && initialAlertState) {
setAlertState({
setCreateAlertState({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.basicAlertState,
});
setThresholdState({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.thresholdState,
});
setEvaluationWindow({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.evaluationWindowState,
});
setAdvancedOptions({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.advancedOptionsState,
});
setNotificationSettings({
type: 'SET_INITIAL_STATE',
payload: initialAlertState.notificationSettingsState,
payload: initialAlertState,
});
}
}, [initialAlertState, isEditMode]);
const discardAlertRule = useCallback(() => {
setAlertState({
type: 'RESET',
});
setThresholdState({
type: 'RESET',
});
setEvaluationWindow({
type: 'RESET',
});
setAdvancedOptions({
type: 'RESET',
});
setNotificationSettings({
setCreateAlertState({
type: 'RESET',
});
handleAlertTypeChange(AlertTypes.METRICS_BASED_ALERT);
@@ -212,17 +227,17 @@ export function CreateAlertProvider(
const contextValue: ICreateAlertContextProps = useMemo(
() => ({
alertState,
alertState: createAlertState.basic,
setAlertState,
alertType,
setAlertType: handleAlertTypeChange,
thresholdState,
thresholdState: createAlertState.threshold,
setThresholdState,
evaluationWindow,
evaluationWindow: createAlertState.evaluationWindow,
setEvaluationWindow,
advancedOptions,
advancedOptions: createAlertState.advancedOptions,
setAdvancedOptions,
notificationSettings,
notificationSettings: createAlertState.notificationSettings,
setNotificationSettings,
discardAlertRule,
createAlertRule,
@@ -234,13 +249,14 @@ export function CreateAlertProvider(
isEditMode: isEditMode || false,
}),
[
alertState,
createAlertState,
setAlertState,
setThresholdState,
setEvaluationWindow,
setAdvancedOptions,
setNotificationSettings,
alertType,
handleAlertTypeChange,
thresholdState,
evaluationWindow,
advancedOptions,
notificationSettings,
discardAlertRule,
createAlertRule,
isCreatingAlertRule,

View File

@@ -9,8 +9,6 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
import { Labels } from 'types/api/alerts/def';
import { GetCreateAlertLocalStateFromAlertDefReturn } from '../types';
export interface ICreateAlertContextProps {
alertState: AlertState;
setAlertState: Dispatch<CreateAlertAction>;
@@ -52,7 +50,7 @@ export interface ICreateAlertContextProps {
export interface ICreateAlertProviderProps {
children: React.ReactNode;
initialAlertType: AlertTypes;
initialAlertState?: GetCreateAlertLocalStateFromAlertDefReturn;
initialAlertState?: CreateAlertState;
isEditMode?: boolean;
ruleId?: string;
}
@@ -272,3 +270,31 @@ export type NotificationSettingsAction =
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
| { type: 'SET_INITIAL_STATE'; payload: NotificationSettingsState }
| { type: 'RESET' };
export type CreateAlertState = {
basic: AlertState;
threshold: AlertThresholdState;
advancedOptions: AdvancedOptionsState;
evaluationWindow: EvaluationWindowState;
notificationSettings: NotificationSettingsState;
};
export enum CreateAlertSlice {
BASIC = 'basic',
THRESHOLD = 'threshold',
ADVANCED_OPTIONS = 'advancedOptions',
EVALUATION_WINDOW = 'evaluationWindow',
NOTIFICATION_SETTINGS = 'notificationSettings',
}
export type CreateAlertReducerAction =
| { slice: CreateAlertSlice.BASIC; action: CreateAlertAction }
| { slice: CreateAlertSlice.THRESHOLD; action: AlertThresholdAction }
| { slice: CreateAlertSlice.ADVANCED_OPTIONS; action: AdvancedOptionsAction }
| { slice: CreateAlertSlice.EVALUATION_WINDOW; action: EvaluationWindowAction }
| {
slice: CreateAlertSlice.NOTIFICATION_SETTINGS;
action: NotificationSettingsAction;
}
| { type: 'RESET' }
| { type: 'SET_INITIAL_STATE'; payload: CreateAlertState };

View File

@@ -18,6 +18,7 @@ import {
INITIAL_ADVANCED_OPTIONS_STATE,
INITIAL_ALERT_STATE,
INITIAL_ALERT_THRESHOLD_STATE,
INITIAL_CREATE_ALERT_STATE,
INITIAL_EVALUATION_WINDOW_STATE,
INITIAL_NOTIFICATION_SETTINGS_STATE,
} from './constants';
@@ -28,6 +29,9 @@ import {
AlertThresholdAction,
AlertThresholdState,
CreateAlertAction,
CreateAlertReducerAction,
CreateAlertSlice,
CreateAlertState,
EvaluationWindowAction,
EvaluationWindowState,
NotificationSettingsAction,
@@ -251,3 +255,57 @@ export const notificationSettingsReducer = (
return state;
}
};
export const createAlertReducer = (
state: CreateAlertState,
action: CreateAlertReducerAction,
): CreateAlertState => {
// Global actions
if ('type' in action) {
switch (action.type) {
case 'RESET':
return INITIAL_CREATE_ALERT_STATE;
case 'SET_INITIAL_STATE':
return action.payload;
default:
return state;
}
}
// Slice actions
switch (action.slice) {
case CreateAlertSlice.BASIC:
return { ...state, basic: alertCreationReducer(state.basic, action.action) };
case CreateAlertSlice.THRESHOLD:
return {
...state,
threshold: alertThresholdReducer(state.threshold, action.action),
};
case CreateAlertSlice.ADVANCED_OPTIONS:
return {
...state,
advancedOptions: advancedOptionsReducer(
state.advancedOptions,
action.action,
),
};
case CreateAlertSlice.EVALUATION_WINDOW:
return {
...state,
evaluationWindow: evaluationWindowReducer(
state.evaluationWindow,
action.action,
),
};
case CreateAlertSlice.NOTIFICATION_SETTINGS:
return {
...state,
notificationSettings: notificationSettingsReducer(
state.notificationSettings,
action.action,
),
};
default:
return state;
}
};

View File

@@ -21,11 +21,11 @@ import {
AlertThresholdMatchType,
AlertThresholdOperator,
AlertThresholdState,
CreateAlertState,
EvaluationWindowState,
NotificationSettingsState,
} from './context/types';
import { EVALUATION_WINDOW_TIMEFRAME } from './EvaluationSettings/constants';
import { GetCreateAlertLocalStateFromAlertDefReturn } from './types';
export function Spinner(): JSX.Element | null {
const { isCreatingAlertRule, isUpdatingAlertRule } = useCreateAlertState();
@@ -265,14 +265,14 @@ export function getThresholdStateFromAlertDef(
export function getCreateAlertLocalStateFromAlertDef(
alertDef: PostableAlertRuleV2 | undefined,
): GetCreateAlertLocalStateFromAlertDefReturn {
): CreateAlertState {
if (!alertDef) {
return {
basicAlertState: INITIAL_ALERT_STATE,
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
advancedOptionsState: INITIAL_ADVANCED_OPTIONS_STATE,
evaluationWindowState: INITIAL_EVALUATION_WINDOW_STATE,
notificationSettingsState: INITIAL_NOTIFICATION_SETTINGS_STATE,
basic: INITIAL_ALERT_STATE,
threshold: INITIAL_ALERT_THRESHOLD_STATE,
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
};
}
// Basic alert state
@@ -294,10 +294,10 @@ export function getCreateAlertLocalStateFromAlertDef(
);
return {
basicAlertState,
thresholdState,
advancedOptionsState,
evaluationWindowState,
notificationSettingsState,
basic: basicAlertState,
threshold: thresholdState,
advancedOptions: advancedOptionsState,
evaluationWindow: evaluationWindowState,
notificationSettings: notificationSettingsState,
};
}

View File

@@ -22,7 +22,7 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { PenLine, Trash2 } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { TVariableMode } from './types';

View File

@@ -1,8 +1,11 @@
import { memo, useEffect, useState } from 'react';
import { memo, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Row } from 'antd';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import {
useDashboardVariables,
useDashboardVariablesSelector,
} from 'hooks/dashboard/useDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { isEmpty } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
@@ -12,13 +15,7 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import DynamicVariableSelection from './DynamicVariableSelection';
import {
buildDependencies,
buildDependencyGraph,
buildParentDependencyGraph,
IDependencyData,
onUpdateVariableNode,
} from './util';
import { onUpdateVariableNode } from './util';
import VariableItem from './VariableItem';
import './DashboardVariableSelection.styles.scss';
@@ -35,11 +32,11 @@ function DashboardVariableSelection(): JSX.Element | null {
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const [dependencyData, setDependencyData] = useState<IDependencyData | null>(
null,
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
const dependencyData = useDashboardVariablesSelector(
(state) => state.dependencyData,
);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
@@ -47,24 +44,6 @@ function DashboardVariableSelection(): JSX.Element | null {
);
useEffect(() => {
const tableRowData = [];
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(dashboardVariables)) {
const { id } = value;
tableRowData.push({
key,
name: key,
...dashboardVariables[key],
id,
});
}
tableRowData.sort((a, b) => a.order - b.order);
setVariablesTableData(tableRowData);
// Initialize variables with default values if not in URL
initializeDefaultVariables(
dashboardVariables,
@@ -73,30 +52,6 @@ function DashboardVariableSelection(): JSX.Element | null {
);
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
useEffect(() => {
if (variablesTableData.length > 0) {
const depGrp = buildDependencies(variablesTableData);
const { order, graph, hasCycle, cycleNodes } = buildDependencyGraph(depGrp);
const parentDependencyGraph = buildParentDependencyGraph(graph);
// cleanup order to only include variables that are of type 'QUERY'
const cleanedOrder = order.filter((variable) => {
const variableData = variablesTableData.find(
(v: IDashboardVariable) => v.name === variable,
);
return variableData?.type === 'QUERY';
});
setDependencyData({
order: cleanedOrder,
graph,
parentDependencyGraph,
hasCycle,
cycleNodes,
});
}
}, [dashboardVariables, variablesTableData]);
// this handles the case where the dependency order changes i.e. variable list updated via creation or deletion etc. and we need to refetch the variables
// also trigger when the global time changes
useEffect(
@@ -186,45 +141,30 @@ function DashboardVariableSelection(): JSX.Element | null {
}
};
if (!dashboardVariables) {
return null;
}
const orderBasedSortedVariables = variablesTableData.sort(
(a: { order: number }, b: { order: number }) => a.order - b.order,
);
return (
<Row style={{ display: 'flex', gap: '12px' }}>
{orderBasedSortedVariables &&
Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) =>
variable.type === 'DYNAMIC' ? (
<DynamicVariableSelection
key={`${variable.name}${variable.id}${variable.order}`}
existingVariables={dashboardVariables}
variableData={{
name: variable.name,
...variable,
}}
onValueUpdate={onValueUpdate}
/>
) : (
<VariableItem
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={dashboardVariables}
variableData={{
name: variable.name,
...variable,
}}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
),
)}
{sortedVariablesArray.map((variable) => {
const key = `${variable.name}${variable.id}${variable.order}`;
return variable.type === 'DYNAMIC' ? (
<DynamicVariableSelection
key={key}
existingVariables={dashboardVariables}
variableData={variable}
onValueUpdate={onValueUpdate}
/>
) : (
<VariableItem
key={key}
existingVariables={dashboardVariables}
variableData={variable}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
);
})}
</Row>
);
}

View File

@@ -16,6 +16,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isEmpty, isString } from 'lodash-es';
import { IDependencyData } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { AppState } from 'store/reducers';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
@@ -24,7 +25,7 @@ import { popupContainer } from 'utils/selectPopupContainer';
import { ALL_SELECT_VALUE, variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle } from './styles';
import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util';
import { areArraysEqual, checkAPIInvocation } from './util';
import './DashboardVariableSelection.styles.scss';

View File

@@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { v4 as uuidv4 } from 'uuid';

View File

@@ -1,6 +1,9 @@
import { OptionData } from 'components/NewSelect/types';
import { isEmpty, isNull } from 'lodash-es';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import {
IDashboardVariables,
IDependencyData,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
export function areArraysEqual(
@@ -97,14 +100,6 @@ export const buildDependencies = (
return graph;
};
export interface IDependencyData {
order: string[];
graph: VariableGraph;
parentDependencyGraph: VariableGraph;
hasCycle: boolean;
cycleNodes?: string[];
}
export const buildParentDependencyGraph = (
graph: VariableGraph,
): VariableGraph => {

View File

@@ -0,0 +1,104 @@
import { useCallback, useRef } from 'react';
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
import Legend from 'lib/uPlotV2/components/Legend/Legend';
import Tooltip from 'lib/uPlotV2/components/Tooltip/Tooltip';
import {
LegendPosition,
TooltipRenderArgs,
} from 'lib/uPlotV2/components/types';
import UPlotChart from 'lib/uPlotV2/components/UPlotChart';
import { PlotContextProvider } from 'lib/uPlotV2/context/PlotContext';
import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
import _noop from 'lodash-es/noop';
import uPlot from 'uplot';
import { ChartProps } from '../types';
const TOOLTIP_WIDTH_PADDING = 60;
const TOOLTIP_MIN_WIDTH = 200;
export default function TimeSeries({
legendConfig = { position: LegendPosition.BOTTOM },
config,
data,
width: containerWidth,
height: containerHeight,
disableTooltip = false,
canPinTooltip = false,
timezone,
yAxisUnit,
decimalPrecision,
syncMode,
syncKey,
onDestroy = _noop,
children,
layoutChildren,
'data-testid': testId,
}: ChartProps): JSX.Element {
const plotInstanceRef = useRef<uPlot | null>(null);
const legendComponent = useCallback(
(averageLegendWidth: number): React.ReactNode => {
return (
<Legend
config={config}
position={legendConfig.position}
averageLegendWidth={averageLegendWidth}
/>
);
},
[config, legendConfig.position],
);
return (
<PlotContextProvider>
<ChartLayout
config={config}
containerWidth={containerWidth}
containerHeight={containerHeight}
legendConfig={legendConfig}
legendComponent={legendComponent}
layoutChildren={layoutChildren}
>
{({ chartWidth, chartHeight, averageLegendWidth }): JSX.Element => (
<UPlotChart
config={config}
data={data}
width={chartWidth}
height={chartHeight}
plotRef={(plot): void => {
plotInstanceRef.current = plot;
}}
onDestroy={(plot: uPlot): void => {
plotInstanceRef.current = null;
onDestroy(plot);
}}
data-testid={testId}
>
{children}
{!disableTooltip && (
<TooltipPlugin
config={config}
canPinTooltip={canPinTooltip}
syncMode={syncMode}
maxWidth={Math.max(
TOOLTIP_MIN_WIDTH,
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
)}
syncKey={syncKey}
render={(props: TooltipRenderArgs): React.ReactNode => (
<Tooltip
{...props}
timezone={timezone}
yAxisUnit={yAxisUnit}
decimalPrecision={decimalPrecision}
/>
)}
/>
)}
</UPlotChart>
)}
</ChartLayout>
</PlotContextProvider>
);
}

View File

@@ -0,0 +1,29 @@
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
interface BaseChartProps {
width: number;
height: number;
disableTooltip?: boolean;
timezone: string;
syncMode?: DashboardCursorSync;
syncKey?: string;
canPinTooltip?: boolean;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
}
interface TimeSeriesChartProps extends BaseChartProps {
config: UPlotConfigBuilder;
legendConfig: LegendConfig;
data: uPlot.AlignedData;
plotRef?: (plot: uPlot | null) => void;
onDestroy?: (plot: uPlot) => void;
children?: React.ReactNode;
layoutChildren?: React.ReactNode;
'data-testid'?: string;
}
export type ChartProps = TimeSeriesChartProps;

View File

@@ -5,25 +5,32 @@ export interface ChartDimensions {
height: number;
legendWidth: number;
legendHeight: number;
legendsPerSet: number;
averageLegendWidth: number;
}
const AVG_CHAR_WIDTH = 8;
const LEGEND_WIDTH_PERCENTILE = 0.85;
const DEFAULT_AVG_LABEL_LENGTH = 15;
const LEGEND_GAP = 16;
const BASE_LEGEND_WIDTH = 16;
const LEGEND_PADDING = 12;
const LEGEND_LINE_HEIGHT = 36;
const LEGEND_LINE_HEIGHT = 28;
/**
* Average text width from series labels (for legendsPerSet).
* Calculates the average width of the legend items based on the labels of the series.
* @param legends - The labels of the series.
* @returns The average width of the legend items.
*/
export function calculateAverageLegendWidth(legends: string[]): number {
if (legends.length === 0) {
return DEFAULT_AVG_LABEL_LENGTH;
return DEFAULT_AVG_LABEL_LENGTH * AVG_CHAR_WIDTH;
}
const averageLabelLength =
legends.reduce((sum, l) => sum + l.length, 0) / legends.length;
return averageLabelLength * AVG_CHAR_WIDTH;
const lengths = legends.map((l) => l.length).sort((a, b) => a - b);
const index = Math.ceil(LEGEND_WIDTH_PERCENTILE * lengths.length) - 1;
const percentileLength = lengths[Math.max(0, index)];
return BASE_LEGEND_WIDTH + percentileLength * AVG_CHAR_WIDTH;
}
/**
@@ -64,7 +71,7 @@ export function calculateChartDimensions({
height: 0,
legendWidth: 0,
legendHeight: 0,
legendsPerSet: 0,
averageLegendWidth: 0,
};
}
@@ -85,13 +92,15 @@ export function calculateChartDimensions({
legendWidth: rightLegendWidth,
legendHeight: containerHeight,
// Single vertical list on the right.
legendsPerSet: 1,
averageLegendWidth: rightLegendWidth,
};
}
const legendRowHeight = LEGEND_LINE_HEIGHT + LEGEND_PADDING;
const legendItemWidth = Math.min(approxLegendItemWidth, 400);
const legendItemWidth = Math.ceil(
Math.min(approxLegendItemWidth, MAX_LEGEND_WIDTH),
);
const legendItemsPerRow = Math.max(
1,
Math.floor((containerWidth - LEGEND_PADDING * 2) / legendItemWidth),
@@ -114,17 +123,11 @@ export function calculateChartDimensions({
maxAllowedLegendHeight,
);
// How many legend items per row in the Legend component.
const legendsPerSet = Math.ceil(
(containerWidth + LEGEND_GAP) /
(Math.min(MAX_LEGEND_WIDTH, approxLegendItemWidth) + LEGEND_GAP),
);
return {
width: containerWidth,
height: Math.max(0, containerHeight - bottomLegendHeight),
legendWidth: containerWidth,
legendHeight: bottomLegendHeight,
legendsPerSet,
averageLegendWidth: legendItemWidth,
};
}

View File

@@ -10,7 +10,6 @@
.chart-layout__legend-wrapper {
padding-left: 0 !important;
padding-right: 12px !important;
}
}

View File

@@ -11,6 +11,7 @@ export interface ChartLayoutProps {
children: (props: {
chartWidth: number;
chartHeight: number;
averageLegendWidth: number;
}) => React.ReactNode;
layoutChildren?: React.ReactNode;
containerWidth: number;
@@ -56,6 +57,7 @@ export default function ChartLayout({
{children({
chartWidth: chartDimensions.width,
chartHeight: chartDimensions.height,
averageLegendWidth: chartDimensions.averageLegendWidth,
})}
</div>
<div
@@ -65,7 +67,7 @@ export default function ChartLayout({
width: chartDimensions.legendWidth,
}}
>
{legendComponent(chartDimensions.legendsPerSet)}
{legendComponent(chartDimensions.averageLegendWidth)}
</div>
</div>
{layoutChildren}

View File

@@ -13,3 +13,16 @@ export interface GraphVisibilityState {
name: string;
dataIndex: SeriesVisibilityItem[];
}
/**
* Context in which a panel is rendered. Used to vary behavior (e.g. persistence,
* interactions) per context.
*/
export enum PanelMode {
/** Panel opened in full-screen / standalone view (e.g. from a dashboard widget). */
STANDALONE_VIEW = 'STANDALONE_VIEW',
/** Panel in the widget builder while editing a dashboard. */
DASHBOARD_EDIT = 'DASHBOARD_EDIT',
/** Panel rendered as a widget on a dashboard (read-only view). */
DASHBOARD_VIEW = 'DASHBOARD_VIEW',
}

View File

@@ -2,6 +2,7 @@
import { MutableRefObject } from 'react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { render, screen, waitFor } from 'tests/test-utils';
@@ -156,6 +157,7 @@ describe('PanelWrapper with DragSelect', () => {
render(
<PanelWrapper
panelMode={PanelMode.STANDALONE_VIEW}
widget={mockWidget}
queryResponse={mockQueryResponse}
onDragSelect={mockOnDragSelect}

View File

@@ -17,6 +17,7 @@ import WarningPopover from 'components/WarningPopover/WarningPopover';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import {
@@ -366,6 +367,7 @@ function FullView({
/>
)}
<PanelWrapper
panelMode={PanelMode.STANDALONE_VIEW}
queryResponse={response}
widget={widget}
setRequestData={setRequestData}

View File

@@ -14,6 +14,7 @@ import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplor
import { ToggleGraphProps } from 'components/Graph/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
@@ -407,6 +408,7 @@ function WidgetGraphComponent({
ref={graphRef}
>
<PanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}

View File

@@ -4,7 +4,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';

View File

@@ -13,6 +13,7 @@ import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplor
import { ToggleGraphProps } from 'components/Graph/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
@@ -183,6 +184,7 @@ function WidgetGraph({
}}
>
<PanelWrapper
panelMode={PanelMode.DASHBOARD_EDIT}
widget={selectedWidget}
queryResponse={queryResponse}
setRequestData={setRequestData}

View File

@@ -21,6 +21,7 @@ function PanelWrapper({
onOpenTraceBtnClick,
customSeries,
customOnRowClick,
panelMode,
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
@@ -33,6 +34,7 @@ function PanelWrapper({
}
return (
<Component
panelMode={panelMode}
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}

View File

@@ -1,3 +1,4 @@
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -11,6 +12,7 @@ describe('Table panel wrappper tests', () => {
it('table should render fine with the query response and column units', () => {
const { container, getByText } = render(
<TablePanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={(tablePanelWidgetQuery as unknown) as Widgets}
queryResponse={(tablePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}

View File

@@ -1,3 +1,4 @@
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -20,6 +21,7 @@ describe('Value panel wrappper tests', () => {
it('should render value panel correctly with yaxis unit', () => {
const { getByText } = render(
<ValuePanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={(valuePanelWidget as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
@@ -34,6 +36,7 @@ describe('Value panel wrappper tests', () => {
it('should render tooltip when there are conflicting thresholds', () => {
const { getByTestId, container } = render(
<ValuePanelWrapper
panelMode={PanelMode.DASHBOARD_VIEW}
widget={({ ...valuePanelWidget, thresholds } as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}

View File

@@ -1,6 +1,7 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { WidgetGraphComponentProps } from 'container/GridCardLayout/GridCard/types';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
@@ -31,6 +32,7 @@ export type PanelWrapperProps = {
customOnRowClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
enableDrillDown?: boolean;
panelMode: PanelMode;
};
export type TooltipData = {

View File

@@ -40,6 +40,7 @@
font-size: 12px;
font-style: normal;
font-weight: 400;
pointer-events: none;
// line-height: 18px;
color: var(--bg-sakura-400) !important;

View File

@@ -96,6 +96,7 @@ export default function QueryFunctions({
const isDarkMode = useIsDarkMode();
const hasAnomalyFunction = functions.some((func) => func.name === 'anomaly');
const hasFunctions = functions.length > 0;
const handleAddNewFunction = (): void => {
const defaultFunctionStruct =
@@ -180,10 +181,14 @@ export default function QueryFunctions({
<div
className={cx(
'query-functions-container',
functions && functions.length > 0 ? 'hasFunctions' : '',
hasFunctions ? 'hasFunctions' : '',
)}
>
<Button className="periscope-btn function-btn">
<Button
className="periscope-btn function-btn"
disabled={hasFunctions}
onClick={handleAddNewFunction}
>
<FunctionIcon
className="function-icon"
fillColor={!isDarkMode ? '#0B0C0E' : 'white'}

View File

@@ -12,7 +12,7 @@ import {
initialQueriesMap,
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import { IUseDashboardVariablesReturn } from 'hooks/dashboard/useDashboardVariables';
import { IUseDashboardVariablesReturn } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { renderHook } from '@testing-library/react';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import useGetResolvedText from '../useGetResolvedText';

View File

@@ -1,21 +1,40 @@
import { useSyncExternalStore } from 'react';
import { useCallback, useRef, useSyncExternalStore } from 'react';
import { dashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
dashboardVariablesStore,
IDashboardVariables,
} from '../../providers/Dashboard/store/dashboardVariablesStore';
IDashboardVariablesStoreState,
IUseDashboardVariablesReturn,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
export interface IUseDashboardVariablesReturn {
dashboardVariables: IDashboardVariables;
}
/**
* Generic selector hook for dashboard variables store
* Allows granular subscriptions to any part of the store state
*
* @example
* ! Select top-level field
* const variables = useDashboardVariablesSelector(s => s.variables);
*
* ! Select specific variable
* const fooVar = useDashboardVariablesSelector(s => s.variables['foo']);
*
* ! Select derived value
* const hasVariables = useDashboardVariablesSelector(s => Object.keys(s.variables).length > 0);
*/
export const useDashboardVariablesSelector = <T>(
selector: (state: IDashboardVariablesStoreState) => T,
): T => {
const selectorRef = useRef(selector);
selectorRef.current = selector;
export const useDashboardVariables = (): IUseDashboardVariablesReturn => {
const dashboardVariables = useSyncExternalStore(
dashboardVariablesStore.subscribe,
dashboardVariablesStore.getSnapshot,
const getSnapshot = useCallback(
() => selectorRef.current(dashboardVariablesStore.getSnapshot()),
[],
);
return {
dashboardVariables,
};
return useSyncExternalStore(dashboardVariablesStore.subscribe, getSnapshot);
};
export const useDashboardVariables = (): IUseDashboardVariablesReturn => {
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
return { dashboardVariables };
};

View File

@@ -1,5 +1,5 @@
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariablesStore';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import store from 'store';
export const getDashboardVariables = (

View File

@@ -1,7 +1,18 @@
.legend-search-container {
flex-shrink: 0;
width: 100%;
padding-right: 8px;
.legend-search-input {
font-size: 12px;
}
}
.legend-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
height: 100%;
width: 100%;
@@ -17,6 +28,38 @@
height: 100%;
width: 100%;
.virtuoso-grid-list {
min-width: 0;
display: grid;
grid-auto-flow: row;
grid-template-columns: repeat(
auto-fill,
minmax(var(--legend-average-width, 240px), 1fr)
);
row-gap: 4px;
column-gap: 12px;
}
.virtuoso-grid-item {
min-width: 0;
}
&.legend-virtuoso-container-right {
.virtuoso-grid-list {
grid-template-columns: 1fr;
}
}
&.legend-virtuoso-container-single-row {
.virtuoso-grid-list {
grid-template-columns: repeat(
auto-fit,
minmax(var(--legend-average-width, 240px), max-content)
);
justify-content: center;
}
}
&::-webkit-scrollbar {
width: 0.3rem;
}
@@ -58,9 +101,15 @@
align-items: center;
gap: 6px;
padding: 4px 8px;
max-width: 100%;
overflow: hidden;
border-radius: 4px;
cursor: pointer;
&.legend-item-right {
width: 100%;
}
&.legend-item-off {
opacity: 0.3;
text-decoration: line-through;

View File

@@ -1,23 +1,21 @@
import { useCallback, useMemo, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { Tooltip as AntdTooltip } from 'antd';
import { useCallback, useMemo, useRef, useState } from 'react';
import { VirtuosoGrid } from 'react-virtuoso';
import { Input, Tooltip as AntdTooltip } from 'antd';
import cx from 'classnames';
import { LegendItem } from 'lib/uPlotV2/config/types';
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
import { LegendPosition } from 'types/api/dashboard/getAll';
import { LegendProps } from '../types';
import { LegendPosition, LegendProps } from '../types';
import { useLegendActions } from './useLegendActions';
import './Legend.styles.scss';
export const MAX_LEGEND_WIDTH = 320;
const LEGENDS_PER_SET_DEFAULT = 5;
export const MAX_LEGEND_WIDTH = 240;
export default function Legend({
position = LegendPosition.BOTTOM,
config,
legendsPerSet = LEGENDS_PER_SET_DEFAULT,
averageLegendWidth = MAX_LEGEND_WIDTH,
}: LegendProps): JSX.Element {
const {
legendItemsMap,
@@ -33,54 +31,54 @@ export default function Legend({
focusedSeriesIndex,
});
const legendContainerRef = useRef<HTMLDivElement | null>(null);
const [legendSearchQuery, setLegendSearchQuery] = useState('');
// Chunk legend items into rows of LEGENDS_PER_ROW items each
const legendRows = useMemo(() => {
const legendItems = Object.values(legendItemsMap);
const legendItems = useMemo(() => Object.values(legendItemsMap), [
legendItemsMap,
]);
return legendItems.reduce((acc: LegendItem[][], curr, i) => {
if (i % legendsPerSet === 0) {
acc.push([]);
}
acc[acc.length - 1].push(curr);
return acc;
}, [] as LegendItem[][]);
}, [legendItemsMap, legendsPerSet]);
const isSingleRow = useMemo(() => {
if (!legendContainerRef.current || position !== LegendPosition.BOTTOM) {
return false;
}
const containerWidth = legendContainerRef.current.clientWidth;
const renderLegendRow = useCallback(
(rowIndex: number, row: LegendItem[]): JSX.Element => (
<div
key={rowIndex}
className={cx(
'legend-row',
`legend-row-${position.toLowerCase()}`,
legendRows.length === 1 && position === LegendPosition.BOTTOM
? 'legend-single-row'
: '',
)}
>
{row.map((item) => (
<AntdTooltip key={item.seriesIndex} title={item.label}>
<div
data-legend-item-id={item.seriesIndex}
className={cx('legend-item', {
'legend-item-off': !item.show,
'legend-item-focused': focusedSeriesIndex === item.seriesIndex,
})}
style={{ maxWidth: `min(${MAX_LEGEND_WIDTH}px, 100%)` }}
>
<div
className="legend-marker"
style={{ borderColor: String(item.color) }}
data-is-legend-marker={true}
/>
<span className="legend-label">{item.label}</span>
</div>
</AntdTooltip>
))}
</div>
const totalLegendWidth = legendItems.length * (averageLegendWidth + 16);
const totalRows = Math.ceil(totalLegendWidth / containerWidth);
return totalRows <= 1;
}, [averageLegendWidth, legendContainerRef, legendItems.length, position]);
const visibleLegendItems = useMemo(() => {
if (position !== LegendPosition.RIGHT || !legendSearchQuery.trim()) {
return legendItems;
}
const query = legendSearchQuery.trim().toLowerCase();
return legendItems.filter((item) =>
item.label?.toLowerCase().includes(query),
);
}, [position, legendSearchQuery, legendItems]);
const renderLegendItem = useCallback(
(item: LegendItem): JSX.Element => (
<AntdTooltip key={item.seriesIndex} title={item.label}>
<div
data-legend-item-id={item.seriesIndex}
className={cx('legend-item', `legend-item-${position.toLowerCase()}`, {
'legend-item-off': !item.show,
'legend-item-focused': focusedSeriesIndex === item.seriesIndex,
})}
>
<div
className="legend-marker"
style={{ borderColor: String(item.color) }}
data-is-legend-marker={true}
/>
<span className="legend-label">{item.label}</span>
</div>
</AntdTooltip>
),
[focusedSeriesIndex, position, legendRows],
[focusedSeriesIndex, position],
);
return (
@@ -90,11 +88,29 @@ export default function Legend({
onClick={onLegendClick}
onMouseMove={onLegendMouseMove}
onMouseLeave={onLegendMouseLeave}
style={{
['--legend-average-width' as string]: `${averageLegendWidth + 16}px`, // 16px is the marker width
}}
>
<Virtuoso
className="legend-virtuoso-container"
data={legendRows}
itemContent={(index, row): JSX.Element => renderLegendRow(index, row)}
{position === LegendPosition.RIGHT && (
<div className="legend-search-container">
<Input
allowClear
placeholder="Search..."
value={legendSearchQuery}
onChange={(e): void => setLegendSearchQuery(e.target.value)}
className="legend-search-input"
/>
</div>
)}
<VirtuosoGrid
className={cx(
'legend-virtuoso-container',
`legend-virtuoso-container-${position.toLowerCase()}`,
{ 'legend-virtuoso-container-single-row': isSingleRow },
)}
data={visibleLegendItems}
itemContent={(_, item): JSX.Element => renderLegendItem(item)}
/>
</div>
);

View File

@@ -5,7 +5,7 @@
-webkit-font-smoothing: antialiased;
color: var(--bg-vanilla-100);
border-radius: 6px;
padding: 1rem 1rem 0.5rem 1rem;
padding: 1rem 0.5rem 0.5rem 1rem;
border: 1px solid var(--bg-ink-100);
display: flex;
flex-direction: column;
@@ -15,6 +15,12 @@
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
border: 1px solid var(--bg-vanilla-300);
.uplot-tooltip-list {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-400);
}
}
}
.uplot-tooltip-header {
@@ -22,18 +28,18 @@
font-weight: 500;
}
.uplot-tooltip-list-container {
height: 100%;
.uplot-tooltip-list {
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
}
.uplot-tooltip-list {
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-100);
border-radius: 0.5rem;
}
}

View File

@@ -6,7 +6,6 @@ import { LineChart } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import uPlot, { AlignedData, Options } from 'uplot';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
import { usePlotContext } from '../context/PlotContext';
import { UPlotChartProps } from './types';
@@ -49,7 +48,6 @@ export default function UPlotChart({
const containerRef = useRef<HTMLDivElement>(null);
const plotInstanceRef = useRef<uPlot | null>(null);
const prevPropsRef = useRef<UPlotChartProps | null>(null);
const configUsedForPlotRef = useRef<UPlotConfigBuilder | null>(null);
/**
* Destroy the existing plot instance if present.
@@ -57,12 +55,6 @@ export default function UPlotChart({
const destroyPlot = useCallback((): void => {
if (plotInstanceRef.current) {
onDestroy?.(plotInstanceRef.current);
// Clean up the config builder that was used to create this plot (not the current prop)
if (configUsedForPlotRef.current) {
configUsedForPlotRef.current.destroy();
}
configUsedForPlotRef.current = null;
plotInstanceRef.current.destroy();
plotInstanceRef.current = null;
setPlotContextInitialState({ uPlotInstance: null });
@@ -97,13 +89,14 @@ export default function UPlotChart({
if (plotRef) {
plotRef(plot);
}
setPlotContextInitialState({
uPlotInstance: plot,
widgetId: config.getWidgetId(),
shouldSaveSelectionPreference: config.getShouldSaveSelectionPreference(),
});
plotInstanceRef.current = plot;
configUsedForPlotRef.current = config;
}, [
config,
data,
@@ -143,11 +136,13 @@ export default function UPlotChart({
return;
}
// Check if the plot instance's container has been unmounted (e.g., after "No Data" state)
// If so, we need to recreate the plot with the new container
// Check if the plot instance's root is no longer attached to our container
// (e.g., after React has remounted the container div). In uPlot, `root`
// is a child of the container, so we must compare against its parent node.
const isPlotOrphaned =
plotInstanceRef.current &&
plotInstanceRef.current.root !== containerRef.current;
!!plotInstanceRef.current &&
(!containerRef.current ||
plotInstanceRef.current.root.parentElement !== containerRef.current);
// Update dimensions without reinitializing if only size changed
if (

View File

@@ -75,7 +75,7 @@ export interface LegendConfig {
export interface LegendProps {
position?: LegendPosition;
config: UPlotConfigBuilder;
legendsPerSet?: number;
averageLegendWidth?: number;
}
export interface TooltipContentItem {

View File

@@ -1,6 +1,7 @@
import { getStoredSeriesVisibility } from 'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { thresholdsDrawHook } from 'lib/uPlotV2/hooks/useThresholdsDrawHook';
import { resolveSeriesVisibility } from 'lib/uPlotV2/utils/seriesVisibility';
import { merge } from 'lodash-es';
import noop from 'lodash-es/noop';
import uPlot, { Cursor, Hooks, Options } from 'uplot';
@@ -11,6 +12,7 @@ import {
DEFAULT_CURSOR_CONFIG,
DEFAULT_PLOT_CONFIG,
LegendItem,
SelectionPreferencesSource,
} from './types';
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
@@ -36,6 +38,10 @@ export class UPlotConfigBuilder extends ConfigBuilder<
> {
series: UPlotSeriesBuilder[] = [];
private selectionPreferencesSource: SelectionPreferencesSource =
SelectionPreferencesSource.IN_MEMORY;
private shouldSaveSelectionPreference = false;
private axes: Record<string, UPlotAxisBuilder> = {};
readonly scales: UPlotScaleBuilder[] = [];
@@ -64,11 +70,15 @@ export class UPlotConfigBuilder extends ConfigBuilder<
private onDragSelect: (startTime: number, endTime: number) => void;
private cleanups: Array<() => void> = [];
constructor(args?: ConfigBuilderProps) {
super(args ?? {});
const { widgetId, onDragSelect, tzDate } = args ?? {};
const {
widgetId,
onDragSelect,
tzDate,
selectionPreferencesSource,
shouldSaveSelectionPreference,
} = args ?? {};
if (widgetId) {
this.widgetId = widgetId;
}
@@ -77,25 +87,38 @@ export class UPlotConfigBuilder extends ConfigBuilder<
this.tzDate = tzDate;
}
this.onDragSelect = noop;
if (selectionPreferencesSource) {
this.selectionPreferencesSource = selectionPreferencesSource;
}
if (shouldSaveSelectionPreference) {
this.shouldSaveSelectionPreference = shouldSaveSelectionPreference;
}
this.onDragSelect = noop;
if (onDragSelect) {
this.onDragSelect = onDragSelect;
// Add a hook to handle the select event
const cleanup = this.addHook('setSelect', (self: uPlot): void => {
this.addHook('setSelect', (self: uPlot): void => {
const selection = self.select;
// Only trigger onDragSelect when there's an actual drag range (width > 0)
// A click without dragging produces width === 0, which should be ignored
if (selection && selection.width > 0) {
const startTime = self.posToVal(selection.left, 'x');
const endTime = self.posToVal(selection.left + selection.width, 'x');
this.onDragSelect(startTime * 1000, endTime * 1000);
}
});
this.cleanups.push(cleanup);
}
}
/**
* Get the save selection preferences
*/
getShouldSaveSelectionPreference(): boolean {
return this.shouldSaveSelectionPreference;
}
/**
* Add or merge an axis configuration
*/
@@ -158,8 +181,7 @@ export class UPlotConfigBuilder extends ConfigBuilder<
addThresholds(options: ThresholdsDrawHookOptions): void {
if (!this.thresholds[options.scaleKey]) {
this.thresholds[options.scaleKey] = options;
const cleanup = this.addHook('draw', thresholdsDrawHook(options));
this.cleanups.push(cleanup);
this.addHook('draw', thresholdsDrawHook(options));
}
}
@@ -212,20 +234,39 @@ export class UPlotConfigBuilder extends ConfigBuilder<
this.tzDate = tzDate;
}
/**
* Returns stored series visibility map from localStorage when preferences source is LOCAL_STORAGE, otherwise null.
*/
private getStoredVisibilityMap(): Map<string, boolean> | null {
if (
this.widgetId &&
this.selectionPreferencesSource === SelectionPreferencesSource.LOCAL_STORAGE
) {
return getStoredSeriesVisibility(this.widgetId);
}
return null;
}
/**
* Get legend items with visibility state restored from localStorage if available
*/
getLegendItems(): Record<number, LegendItem> {
const visibilityMap = this.widgetId
? getStoredSeriesVisibility(this.widgetId)
: null;
const visibilityMap = this.getStoredVisibilityMap();
const isAnySeriesHidden = !!(
visibilityMap && Array.from(visibilityMap.values()).some((show) => !show)
);
return this.series.reduce((acc, s: UPlotSeriesBuilder, index: number) => {
const seriesConfig = s.getConfig();
const label = seriesConfig.label ?? '';
const seriesIndex = index + 1; // +1 because the first series is the timestamp
// Priority: stored visibility > series config > default (true)
const show = visibilityMap?.get(label) ?? seriesConfig.show ?? true;
const show = resolveSeriesVisibility(
label,
seriesConfig.show,
visibilityMap,
isAnySeriesHidden,
);
acc[seriesIndex] = {
seriesIndex,
@@ -238,13 +279,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
}, {} as Record<number, LegendItem>);
}
/**
* Remove all hooks and cleanup functions
*/
destroy(): void {
this.cleanups.forEach((cleanup) => cleanup());
}
/**
* Get the widget id
*/
@@ -260,9 +294,27 @@ export class UPlotConfigBuilder extends ConfigBuilder<
...DEFAULT_PLOT_CONFIG,
};
const visibilityMap = this.getStoredVisibilityMap();
const isAnySeriesHidden = !!(
visibilityMap && Array.from(visibilityMap.values()).some((show) => !show)
);
config.series = [
{ value: (): string => '' }, // Base series for timestamp
...this.series.map((s) => s.getConfig()),
...this.series.map((s) => {
const series = s.getConfig();
const label = series.label ?? '';
const visible = resolveSeriesVisibility(
label,
series.show,
visibilityMap,
isAnySeriesHidden,
);
return {
...series,
show: visible,
};
}),
];
config.axes = Object.values(this.axes).map((a) => a.getConfig());
config.scales = this.scales.reduce(

View File

@@ -22,6 +22,14 @@ export abstract class ConfigBuilder<P, T> {
merge?(props: Partial<P>): void;
}
/**
* Preferences source for the uPlot config builder
*/
export enum SelectionPreferencesSource {
LOCAL_STORAGE = 'LOCAL_STORAGE',
IN_MEMORY = 'IN_MEMORY',
}
/**
* Props for configuring the uPlot config builder
*/
@@ -29,6 +37,8 @@ export interface ConfigBuilderProps {
widgetId?: string;
onDragSelect?: (startTime: number, endTime: number) => void;
tzDate?: uPlot.LocalDateFromUnix;
selectionPreferencesSource?: SelectionPreferencesSource;
shouldSaveSelectionPreference?: boolean;
}
/**

View File

@@ -9,16 +9,17 @@ import {
import type { SeriesVisibilityItem } from 'container/DashboardContainer/visualization/panels/types';
import { updateSeriesVisibilityToLocalStorage } from 'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils';
import type uPlot from 'uplot';
export interface PlotContextInitialState {
uPlotInstance: uPlot | null;
widgetId?: string;
shouldSaveSelectionPreference?: boolean;
}
export interface IPlotContext {
setPlotContextInitialState: (state: PlotContextInitialState) => void;
onToggleSeriesVisibility: (seriesIndex: number) => void;
onToggleSeriesOnOff: (seriesIndex: number) => void;
onFocusSeries: (seriesIndex: number | null) => void;
syncSeriesVisibilityToLocalStorage: () => void;
}
export const PlotContext = createContext<IPlotContext | null>(null);
@@ -29,68 +30,84 @@ export const PlotContextProvider = ({
const uPlotInstanceRef = useRef<uPlot | null>(null);
const activeSeriesIndex = useRef<number | undefined>(undefined);
const widgetIdRef = useRef<string | undefined>(undefined);
const shouldSavePreferencesRef = useRef<boolean>(false);
const setPlotContextInitialState = useCallback(
({ uPlotInstance, widgetId }: PlotContextInitialState): void => {
({
uPlotInstance,
widgetId,
shouldSaveSelectionPreference,
}: PlotContextInitialState): void => {
uPlotInstanceRef.current = uPlotInstance;
widgetIdRef.current = widgetId;
activeSeriesIndex.current = undefined;
shouldSavePreferencesRef.current = !!shouldSaveSelectionPreference;
},
[],
);
const onToggleSeriesVisibility = useCallback((seriesIndex: number): void => {
const syncSeriesVisibilityToLocalStorage = useCallback((): void => {
const plot = uPlotInstanceRef.current;
if (!plot) {
if (!plot || !widgetIdRef.current) {
return;
}
const isReset = activeSeriesIndex.current === seriesIndex;
activeSeriesIndex.current = isReset ? undefined : seriesIndex;
const seriesVisibility: SeriesVisibilityItem[] = plot.series.map(
(series) => ({
label: series.label ?? '',
show: series.show ?? true,
}),
);
plot.batch(() => {
plot.series.forEach((_, index) => {
if (index === 0) {
return;
}
const currentSeriesIndex = index;
plot.setSeries(currentSeriesIndex, {
show: isReset || currentSeriesIndex === seriesIndex,
});
});
if (widgetIdRef.current) {
const seriesVisibility: SeriesVisibilityItem[] = plot.series.map(
(series) => ({
label: series.label ?? '',
show: series.show ?? true,
}),
);
updateSeriesVisibilityToLocalStorage(widgetIdRef.current, seriesVisibility);
updateSeriesVisibilityToLocalStorage(widgetIdRef.current, seriesVisibility);
}, []);
const onToggleSeriesVisibility = useCallback(
(seriesIndex: number): void => {
const plot = uPlotInstanceRef.current;
if (!plot) {
return;
}
});
}, []);
const onToggleSeriesOnOff = useCallback((seriesIndex: number): void => {
const plot = uPlotInstanceRef.current;
if (!plot) {
return;
}
const isReset = activeSeriesIndex.current === seriesIndex;
activeSeriesIndex.current = isReset ? undefined : seriesIndex;
const series = plot.series[seriesIndex];
if (!series) {
return;
}
plot.setSeries(seriesIndex, { show: !series.show });
if (widgetIdRef.current) {
const seriesVisibility: SeriesVisibilityItem[] = plot.series.map(
(series) => ({
label: series.label ?? '',
show: series.show ?? true,
}),
);
updateSeriesVisibilityToLocalStorage(widgetIdRef.current, seriesVisibility);
}
}, []);
plot.batch(() => {
plot.series.forEach((_, index) => {
if (index === 0) {
return;
}
const currentSeriesIndex = index;
plot.setSeries(currentSeriesIndex, {
show: isReset || currentSeriesIndex === seriesIndex,
});
});
if (widgetIdRef.current && shouldSavePreferencesRef.current) {
syncSeriesVisibilityToLocalStorage();
}
});
},
[syncSeriesVisibilityToLocalStorage],
);
const onToggleSeriesOnOff = useCallback(
(seriesIndex: number): void => {
const plot = uPlotInstanceRef.current;
if (!plot) {
return;
}
const series = plot.series[seriesIndex];
if (!series) {
return;
}
plot.setSeries(seriesIndex, { show: !series.show });
if (widgetIdRef.current && shouldSavePreferencesRef.current) {
syncSeriesVisibilityToLocalStorage();
}
},
[syncSeriesVisibilityToLocalStorage],
);
const onFocusSeries = useCallback((seriesIndex: number | null): void => {
const plot = uPlotInstanceRef.current;
@@ -113,12 +130,14 @@ export const PlotContextProvider = ({
setPlotContextInitialState,
onToggleSeriesOnOff,
onFocusSeries,
syncSeriesVisibilityToLocalStorage,
}),
[
onToggleSeriesVisibility,
setPlotContextInitialState,
onToggleSeriesOnOff,
onFocusSeries,
syncSeriesVisibilityToLocalStorage,
],
);

View File

@@ -0,0 +1,11 @@
export function resolveSeriesVisibility(
label: string,
seriesShow: boolean | undefined | null,
visibilityMap: Map<string, boolean> | null,
isAnySeriesHidden: boolean,
): boolean {
if (isAnySeriesHidden) {
return visibilityMap?.get(label) ?? false;
}
return seriesShow ?? true;
}

View File

@@ -45,8 +45,11 @@ import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import { useDashboardVariables } from '../../hooks/dashboard/useDashboardVariables';
import { setDashboardVariablesStore } from './store/dashboardVariablesStore';
import { useDashboardVariablesSelector } from '../../hooks/dashboard/useDashboardVariables';
import {
setDashboardVariablesStore,
updateDashboardVariablesStore,
} from './store/dashboardVariables/dashboardVariablesStore';
import {
DashboardSortOrder,
IDashboardContext,
@@ -198,14 +201,23 @@ export function DashboardProvider({
: isDashboardWidgetPage?.params.dashboardId) || '';
const [selectedDashboard, setSelectedDashboard] = useState<Dashboard>();
const dashboardVariables = useDashboardVariables();
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
useEffect(() => {
const existingVariables = dashboardVariables;
const updatedVariables = selectedDashboard?.data.variables || {};
if (!isEqual(existingVariables, updatedVariables)) {
setDashboardVariablesStore(updatedVariables);
if (savedDashboardId !== dashboardId) {
setDashboardVariablesStore({
dashboardId,
variables: updatedVariables,
});
} else if (!isEqual(existingVariables, updatedVariables)) {
updateDashboardVariablesStore({
dashboardId,
variables: updatedVariables,
});
}
}, [selectedDashboard]);

View File

@@ -0,0 +1,57 @@
import createStore from '../store';
import { IDashboardVariablesStoreState } from './dashboardVariablesStoreTypes';
import {
computeDerivedValues,
updateDerivedValues,
} from './dashboardVariablesStoreUtils';
const initialState: IDashboardVariablesStoreState = {
dashboardId: '',
variables: {},
sortedVariablesArray: [],
dependencyData: null,
};
export const dashboardVariablesStore = createStore<IDashboardVariablesStoreState>(
initialState,
);
/**
* Set dashboard variables (replaces all variables)
*/
export function setDashboardVariablesStore({
dashboardId,
variables,
}: {
dashboardId: string;
variables: IDashboardVariablesStoreState['variables'];
}): void {
dashboardVariablesStore.set(() => {
return {
dashboardId,
variables,
...computeDerivedValues(variables),
} as IDashboardVariablesStoreState;
});
}
/**
* Update specific dashboard variables (merges with existing)
*/
export function updateDashboardVariablesStore({
dashboardId,
variables,
}: {
dashboardId: string;
variables: IDashboardVariablesStoreState['variables'];
}): void {
dashboardVariablesStore.update((draft) => {
if (draft.dashboardId !== dashboardId) {
// If dashboardId doesn't match, we replace the entire state
draft.dashboardId = dashboardId;
}
draft.variables = variables;
updateDerivedValues(draft);
});
}

View File

@@ -0,0 +1,31 @@
import { IDashboardVariable } from 'types/api/dashboard/getAll';
export type VariableGraph = Record<string, string[]>;
export interface IDependencyData {
order: string[];
graph: VariableGraph;
parentDependencyGraph: VariableGraph;
hasCycle: boolean;
cycleNodes?: string[];
}
export type IDashboardVariables = Record<string, IDashboardVariable>;
export interface IDashboardVariablesStoreState {
// dashboard id
dashboardId: string;
// Raw variables keyed by id/name
variables: IDashboardVariables;
// Derived: sorted array of variables by order
sortedVariablesArray: IDashboardVariable[];
// Derived: dependency data for QUERY variables
dependencyData: IDependencyData | null;
}
export interface IUseDashboardVariablesReturn {
dashboardVariables: IDashboardVariablesStoreState['variables'];
}

View File

@@ -0,0 +1,89 @@
import {
buildDependencies,
buildDependencyGraph,
} from 'container/DashboardContainer/DashboardVariablesSelection/util';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import {
IDashboardVariables,
IDashboardVariablesStoreState,
IDependencyData,
} from './dashboardVariablesStoreTypes';
/**
* Build a sorted array of variables by their order property
*/
export function buildSortedVariablesArray(
variables: IDashboardVariables,
): IDashboardVariable[] {
const sortedVariablesArray: IDashboardVariable[] = [];
Object.values(variables).forEach((value) => {
sortedVariablesArray.push({ ...value });
});
sortedVariablesArray.sort((a, b) => a.order - b.order);
return sortedVariablesArray;
}
/**
* Build dependency data from sorted variables array
* This includes the dependency graph, topological order, and cycle detection
*/
export function buildDependencyData(
sortedVariablesArray: IDashboardVariable[],
): IDependencyData | null {
if (sortedVariablesArray.length === 0) {
return null;
}
const dependencies = buildDependencies(sortedVariablesArray);
const {
order,
graph,
parentDependencyGraph,
hasCycle,
cycleNodes,
} = buildDependencyGraph(dependencies);
// Filter order to only include QUERY type variables
const queryVariableOrder = order.filter((variable: string) => {
const variableData = sortedVariablesArray.find((v) => v.name === variable);
return variableData?.type === 'QUERY';
});
return {
order: queryVariableOrder,
graph,
parentDependencyGraph,
hasCycle,
cycleNodes,
};
}
/**
* Compute derived values from variables
* This is a composition of buildSortedVariablesArray and buildDependencyData
*/
export function computeDerivedValues(
variables: IDashboardVariablesStoreState['variables'],
): Pick<
IDashboardVariablesStoreState,
'sortedVariablesArray' | 'dependencyData'
> {
const sortedVariablesArray = buildSortedVariablesArray(variables);
const dependencyData = buildDependencyData(sortedVariablesArray);
return { sortedVariablesArray, dependencyData };
}
/**
* Update derived values in the store state (for use with immer)
*/
export function updateDerivedValues(
draft: IDashboardVariablesStoreState,
): void {
draft.sortedVariablesArray = buildSortedVariablesArray(draft.variables);
draft.dependencyData = buildDependencyData(draft.sortedVariablesArray);
}

View File

@@ -1,14 +0,0 @@
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import createStore from './store';
// export type IDashboardVariables = DashboardData['variables'];
export type IDashboardVariables = Record<string, IDashboardVariable>;
export const dashboardVariablesStore = createStore<IDashboardVariables>({});
export function setDashboardVariablesStore(
variables: Partial<IDashboardVariables>,
): void {
dashboardVariablesStore.set(() => ({ ...variables }));
}

View File

@@ -569,8 +569,8 @@ func (d *Dispatcher) getOrCreateRoute(receiver string) *dispatch.Route {
route := &dispatch.Route{
RouteOpts: dispatch.RouteOpts{
Receiver: receiver,
GroupWait: 30 * time.Second,
GroupInterval: 5 * time.Minute,
GroupWait: d.route.RouteOpts.GroupWait,
GroupInterval: d.route.RouteOpts.GroupInterval,
GroupByAll: false,
},
Matchers: labels.Matchers{{

View File

@@ -1183,8 +1183,8 @@ func TestDispatcherRaceOnFirstAlertNotDeliveredWhenGroupWaitIsZero(t *testing.T)
route:
group_by: ['alertname']
group_wait: 1h
group_interval: 1h
group_wait: 30s
group_interval: 5m
receiver: 'slack'`
conf, err := config.Load(confData)
if err != nil {
@@ -1308,3 +1308,95 @@ func TestDispatcher_DoMaintenance(t *testing.T) {
require.False(t, isMuted)
require.Empty(t, mutedBy)
}
func TestDispatcher_GetOrCreateRoute(t *testing.T) {
testCases := []struct {
name string
confData string
expectedReceiver string
expectedGroupWait time.Duration
expectedGroupInterval time.Duration
expectedGroupByAll bool
expectedMatchersLen int
expectedMatcherName string
expectedMatcherValue string
}{
{
name: "create route for slack receiver",
confData: `receivers:
- name: 'slack'
- name: 'email'
- name: 'pagerduty'
route:
group_by: ['alertname']
group_wait: 1m
group_interval: 1m
receiver: 'slack'`,
expectedReceiver: "slack",
expectedGroupWait: 1 * time.Minute,
expectedGroupInterval: 1 * time.Minute,
expectedGroupByAll: false,
expectedMatchersLen: 1,
expectedMatcherName: "__receiver__",
expectedMatcherValue: "slack",
},
{
name: "no group_wait and group_interval use default values",
confData: `receivers:
- name: 'slack'
route:
group_by: ['alertname']
receiver: 'slack'`,
expectedReceiver: "slack",
expectedGroupWait: 30 * time.Second,
expectedGroupInterval: 5 * time.Minute,
expectedGroupByAll: false,
expectedMatchersLen: 1,
expectedMatcherName: "__receiver__",
expectedMatcherValue: "slack",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conf, err := config.Load(tc.confData)
if err != nil {
t.Fatal(err)
}
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
defer alerts.Close()
timeout := func(d time.Duration) time.Duration { return time.Duration(0) }
recorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*alertmanagertypes.Alert)}
metrics := NewDispatcherMetrics(false, prometheus.NewRegistry())
store := nfroutingstoretest.NewMockSQLRouteStore()
store.MatchExpectationsInOrder(false)
nfManager, err := rulebasednotification.New(context.Background(), providerSettings, nfmanager.Config{}, store)
if err != nil {
t.Fatal(err)
}
d := NewDispatcher(alerts, route, recorder, marker, timeout, nil, logger, metrics, nfManager, "test-org")
// setup the dispatcher for tests
d.receiverRoutes = map[string]*dispatch.Route{}
newRoute := d.getOrCreateRoute(tc.expectedReceiver)
require.Equal(t, tc.expectedReceiver, newRoute.RouteOpts.Receiver)
require.Equal(t, tc.expectedGroupWait, newRoute.RouteOpts.GroupWait)
require.Equal(t, tc.expectedGroupInterval, newRoute.RouteOpts.GroupInterval)
require.Equal(t, tc.expectedGroupByAll, newRoute.RouteOpts.GroupByAll)
require.Equal(t, tc.expectedMatchersLen, len(newRoute.Matchers))
require.Equal(t, tc.expectedMatcherName, newRoute.Matchers[0].Name)
require.Equal(t, tc.expectedMatcherValue, newRoute.Matchers[0].Value)
})
}
}

View File

@@ -12,60 +12,30 @@ import (
)
const (
urlPathKeyLegacy = "http.url"
serverAddressKeyLegacy = "net.peer.name"
urlPathKey = "url.full"
serverAddressKey = "server.address"
derivedKeyHTTPURL = "http_url" // https://signoz.io/docs/traces-management/guides/derived-fields-spans/#http_url
derivedKeyHTTPHost = "http_host"
)
var defaultStepInterval = 60 * time.Second
type SemconvFieldMapping struct {
LegacyField string
CurrentField string
FieldType telemetrytypes.FieldDataType
Context telemetrytypes.FieldContext
}
var dualSemconvGroupByKeys = map[string][]qbtypes.GroupByKey{
"server": {
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKey,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
var (
groupByKeyHTTPHost = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: derivedKeyHTTPHost,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextSpan,
Signal: telemetrytypes.SignalTraces,
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKeyLegacy,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
}
groupByKeyHTTPURL = qbtypes.GroupByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: derivedKeyHTTPURL,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextSpan,
Signal: telemetrytypes.SignalTraces,
},
},
"url": {
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: urlPathKey,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: urlPathKeyLegacy,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
},
}
}
)
func FilterIntermediateColumns(result *qbtypes.QueryRangeResponse) *qbtypes.QueryRangeResponse {
if result == nil || result.Data.Results == nil {
@@ -114,103 +84,6 @@ func FilterIntermediateColumns(result *qbtypes.QueryRangeResponse) *qbtypes.Quer
return result
}
func MergeSemconvColumns(result *qbtypes.QueryRangeResponse) *qbtypes.QueryRangeResponse {
if result == nil || result.Data.Results == nil {
return result
}
for _, res := range result.Data.Results {
scalarData, ok := res.(*qbtypes.ScalarData)
if !ok {
continue
}
serverAddressKeyIdx := -1
serverAddressKeyLegacyIdx := -1
for i, col := range scalarData.Columns {
if col.Name == serverAddressKey {
serverAddressKeyIdx = i
} else if col.Name == serverAddressKeyLegacy {
serverAddressKeyLegacyIdx = i
}
}
if serverAddressKeyIdx == -1 || serverAddressKeyLegacyIdx == -1 {
continue
}
var newRows [][]any
for _, row := range scalarData.Data {
if len(row) <= serverAddressKeyIdx || len(row) <= serverAddressKeyLegacyIdx {
continue
}
var serverName any
if isValidValue(row[serverAddressKeyIdx]) {
serverName = row[serverAddressKeyIdx]
} else if isValidValue(row[serverAddressKeyLegacyIdx]) {
serverName = row[serverAddressKeyLegacyIdx]
}
if serverName != nil {
newRow := make([]any, len(row)-1)
newRow[0] = serverName
targetIdx := 1
for i, val := range row {
if i != serverAddressKeyLegacyIdx && i != serverAddressKeyIdx {
if targetIdx < len(newRow) {
newRow[targetIdx] = val
targetIdx++
}
}
}
newRows = append(newRows, newRow)
}
}
newColumns := make([]*qbtypes.ColumnDescriptor, len(scalarData.Columns)-1)
targetIdx := 0
for i, col := range scalarData.Columns {
if i == serverAddressKeyIdx {
newCol := &qbtypes.ColumnDescriptor{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKeyLegacy,
FieldDataType: col.FieldDataType,
FieldContext: col.FieldContext,
Signal: col.Signal,
},
QueryName: col.QueryName,
AggregationIndex: col.AggregationIndex,
Meta: col.Meta,
Type: col.Type,
}
newColumns[targetIdx] = newCol
targetIdx++
} else if i != serverAddressKeyLegacyIdx {
newColumns[targetIdx] = col
targetIdx++
}
}
scalarData.Columns = newColumns
scalarData.Data = newRows
}
return result
}
func isValidValue(val any) bool {
if val == nil {
return false
}
if str, ok := val.(string); ok {
return str != "" && str != "n/a"
}
return true
}
func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRangeResponse {
filteredResults := make([]*qbtypes.QueryRangeResponse, 0, len(results))
@@ -261,7 +134,7 @@ func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRange
func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
for _, label := range series.Labels {
if label.Key.Name == serverAddressKeyLegacy || label.Key.Name == serverAddressKey {
if label.Key.Name == derivedKeyHTTPHost {
if strVal, ok := label.Value.(string); ok {
if net.ParseIP(strVal) != nil {
return false
@@ -274,12 +147,10 @@ func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
func shouldIncludeRow(row *qbtypes.RawRow) bool {
if row.Data != nil {
for _, key := range []string{serverAddressKeyLegacy, serverAddressKey} {
if domainVal, ok := row.Data[key]; ok {
if domainStr, ok := domainVal.(string); ok {
if net.ParseIP(domainStr) != nil {
return false
}
if domainVal, ok := row.Data[derivedKeyHTTPHost]; ok {
if domainStr, ok := domainVal.(string); ok {
if net.ParseIP(domainStr) != nil {
return false
}
}
}
@@ -287,8 +158,8 @@ func shouldIncludeRow(row *qbtypes.RawRow) bool {
return true
}
func mergeGroupBy(base, additional []qbtypes.GroupByKey) []qbtypes.GroupByKey {
return append(base, additional...)
func mergeGroupBy(base qbtypes.GroupByKey, additional []qbtypes.GroupByKey) []qbtypes.GroupByKey {
return append([]qbtypes.GroupByKey{base}, additional...)
}
func BuildDomainList(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.QueryRangeRequest, error) {
@@ -354,10 +225,10 @@ func buildEndpointsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Q
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "count_distinct(http.url)"},
{Expression: "count_distinct(http_url)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
},
}
}
@@ -373,7 +244,7 @@ func buildLastSeenQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Qu
{Expression: "max(timestamp)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
},
}
}
@@ -389,7 +260,7 @@ func buildRpsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEn
{Expression: "rate()"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
},
}
}
@@ -407,7 +278,7 @@ func buildErrorQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Query
{Expression: "count()"},
},
Filter: filter,
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
},
}
}
@@ -423,7 +294,7 @@ func buildTotalSpanQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.Q
{Expression: "count()"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
},
}
}
@@ -439,7 +310,7 @@ func buildP99Query(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEn
{Expression: "p99(duration_nano)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
GroupBy: mergeGroupBy(groupByKeyHTTPHost, req.GroupBy),
},
}
}
@@ -462,10 +333,10 @@ func buildEndpointsInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtyp
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "rate(http.url)"},
{Expression: fmt.Sprintf("rate(%s)", derivedKeyHTTPURL)},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["url"], req.GroupBy),
GroupBy: mergeGroupBy(groupByKeyHTTPURL, req.GroupBy),
},
}
}
@@ -519,8 +390,7 @@ func buildLastSeenInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtype
}
func buildBaseFilter(additionalFilter *qbtypes.Filter) *qbtypes.Filter {
baseExpression := fmt.Sprintf("(%s EXISTS OR %s EXISTS) AND kind_string = 'Client'",
urlPathKeyLegacy, urlPathKey)
baseExpression := fmt.Sprintf("%s EXISTS AND kind_string = 'Client'", derivedKeyHTTPURL)
if additionalFilter != nil && additionalFilter.Expression != "" {
// even if it contains kind_string we add with an AND so it doesn't matter if the user is overriding it.

View File

@@ -1,9 +1,10 @@
package thirdpartyapi
import (
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
"testing"
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
@@ -28,7 +29,7 @@ func TestFilterResponse(t *testing.T) {
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Key: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Value: "192.168.1.1",
},
},
@@ -36,7 +37,7 @@ func TestFilterResponse(t *testing.T) {
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Key: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Value: "example.com",
},
},
@@ -60,7 +61,7 @@ func TestFilterResponse(t *testing.T) {
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Key: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Value: "example.com",
},
},
@@ -84,12 +85,12 @@ func TestFilterResponse(t *testing.T) {
Rows: []*qbtypes.RawRow{
{
Data: map[string]any{
"net.peer.name": "192.168.1.1",
derivedKeyHTTPHost: "192.168.1.1",
},
},
{
Data: map[string]any{
"net.peer.name": "example.com",
derivedKeyHTTPHost: "example.com",
},
},
},
@@ -106,7 +107,7 @@ func TestFilterResponse(t *testing.T) {
Rows: []*qbtypes.RawRow{
{
Data: map[string]any{
"net.peer.name": "example.com",
derivedKeyHTTPHost: "example.com",
},
},
},

View File

@@ -5008,7 +5008,6 @@ func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
return
}
result = thirdpartyapi.MergeSemconvColumns(result)
result = thirdpartyapi.FilterIntermediateColumns(result)
// Filter IP addresses if ShowIp is false
@@ -5065,7 +5064,6 @@ func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
return
}
result = thirdpartyapi.MergeSemconvColumns(result)
result = thirdpartyapi.FilterIntermediateColumns(result)
// Filter IP addresses if ShowIp is false

View File

@@ -34,9 +34,9 @@ var (
// StatsRequest represents the payload accepted by the metrics stats endpoint.
type StatsRequest struct {
Filter *qbtypes.Filter `json:"filter,omitempty"`
Start int64 `json:"start"`
End int64 `json:"end"`
Limit int `json:"limit"`
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Limit int `json:"limit" required:"true"`
Offset int `json:"offset"`
OrderBy *qbtypes.OrderBy `json:"orderBy,omitempty"`
}
@@ -96,26 +96,26 @@ func (req *StatsRequest) UnmarshalJSON(data []byte) error {
// Stat represents the summary information returned per metric.
type Stat struct {
MetricName string `json:"metricName"`
Description string `json:"description"`
MetricType metrictypes.Type `json:"type"`
MetricUnit string `json:"unit"`
TimeSeries uint64 `json:"timeseries"`
Samples uint64 `json:"samples"`
MetricName string `json:"metricName" required:"true"`
Description string `json:"description" required:"true"`
MetricType metrictypes.Type `json:"type" required:"true" enum:"gauge,sum,histogram,summary,exponentialhistogram"`
MetricUnit string `json:"unit" required:"true"`
TimeSeries uint64 `json:"timeseries" required:"true"`
Samples uint64 `json:"samples" required:"true"`
}
// StatsResponse represents the aggregated metrics statistics.
type StatsResponse struct {
Metrics []Stat `json:"metrics"`
Total uint64 `json:"total"`
Metrics []Stat `json:"metrics" required:"true" nullable:"true"`
Total uint64 `json:"total" required:"true"`
}
type MetricMetadata struct {
Description string `json:"description"`
MetricType metrictypes.Type `json:"type"`
MetricUnit string `json:"unit"`
Temporality metrictypes.Temporality `json:"temporality"`
IsMonotonic bool `json:"isMonotonic"`
Description string `json:"description" required:"true"`
MetricType metrictypes.Type `json:"type" required:"true" enum:"gauge,sum,histogram,summary,exponentialhistogram"`
MetricUnit string `json:"unit" required:"true"`
Temporality metrictypes.Temporality `json:"temporality" required:"true" enum:"delta,cumulative,unspecified"`
IsMonotonic bool `json:"isMonotonic" required:"true"`
}
// MarshalBinary implements cachetypes.Cacheable interface
@@ -130,21 +130,21 @@ func (m *MetricMetadata) UnmarshalBinary(data []byte) error {
// UpdateMetricMetadataRequest represents the payload for updating metric metadata.
type UpdateMetricMetadataRequest struct {
MetricName string `json:"metricName"`
Type metrictypes.Type `json:"type"`
Description string `json:"description"`
Unit string `json:"unit"`
Temporality metrictypes.Temporality `json:"temporality"`
IsMonotonic bool `json:"isMonotonic"`
MetricName string `json:"metricName" required:"true"`
Type metrictypes.Type `json:"type" required:"true" enum:"gauge,sum,histogram,summary,exponentialhistogram"`
Description string `json:"description" required:"true"`
Unit string `json:"unit" required:"true"`
Temporality metrictypes.Temporality `json:"temporality" required:"true" enum:"delta,cumulative,unspecified"`
IsMonotonic bool `json:"isMonotonic" required:"true"`
}
// TreemapRequest represents the payload for the metrics treemap endpoint.
type TreemapRequest struct {
Filter *qbtypes.Filter `json:"filter,omitempty"`
Start int64 `json:"start"`
End int64 `json:"end"`
Limit int `json:"limit"`
Mode TreemapMode `json:"mode"`
Start int64 `json:"start" required:"true"`
End int64 `json:"end" required:"true"`
Limit int `json:"limit" required:"true"`
Mode TreemapMode `json:"mode" required:"true" enum:"timeseries,samples"`
}
// Validate enforces basic constraints on TreemapRequest.
@@ -210,52 +210,52 @@ func (req *TreemapRequest) UnmarshalJSON(data []byte) error {
// TreemapEntry represents each node in the treemap response.
type TreemapEntry struct {
MetricName string `json:"metricName"`
Percentage float64 `json:"percentage"`
TotalValue uint64 `json:"totalValue"`
MetricName string `json:"metricName" required:"true"`
Percentage float64 `json:"percentage" required:"true"`
TotalValue uint64 `json:"totalValue" required:"true"`
}
// TreemapResponse is the output structure for the treemap endpoint.
type TreemapResponse struct {
TimeSeries []TreemapEntry `json:"timeseries"`
Samples []TreemapEntry `json:"samples"`
TimeSeries []TreemapEntry `json:"timeseries" required:"true" nullable:"true"`
Samples []TreemapEntry `json:"samples" required:"true" nullable:"true"`
}
// MetricAlert represents an alert associated with a metric.
type MetricAlert struct {
AlertName string `json:"alertName"`
AlertID string `json:"alertId"`
AlertName string `json:"alertName" required:"true"`
AlertID string `json:"alertId" required:"true"`
}
// MetricAlertsResponse represents the response for metric alerts endpoint.
type MetricAlertsResponse struct {
Alerts []MetricAlert `json:"alerts"`
Alerts []MetricAlert `json:"alerts" required:"true" nullable:"true"`
}
// MetricDashboard represents a dashboard/widget referencing a metric.
type MetricDashboard struct {
DashboardName string `json:"dashboardName"`
DashboardID string `json:"dashboardId"`
WidgetID string `json:"widgetId"`
WidgetName string `json:"widgetName"`
DashboardName string `json:"dashboardName" required:"true"`
DashboardID string `json:"dashboardId" required:"true"`
WidgetID string `json:"widgetId" required:"true"`
WidgetName string `json:"widgetName" required:"true"`
}
// MetricDashboardsResponse represents the response for metric dashboards endpoint.
type MetricDashboardsResponse struct {
Dashboards []MetricDashboard `json:"dashboards"`
Dashboards []MetricDashboard `json:"dashboards" required:"true" nullable:"true"`
}
// MetricHighlightsResponse is the output structure for the metric highlights endpoint.
type MetricHighlightsResponse struct {
DataPoints uint64 `json:"dataPoints"`
LastReceived uint64 `json:"lastReceived"`
TotalTimeSeries uint64 `json:"totalTimeSeries"`
ActiveTimeSeries uint64 `json:"activeTimeSeries"`
DataPoints uint64 `json:"dataPoints" required:"true"`
LastReceived uint64 `json:"lastReceived" required:"true"`
TotalTimeSeries uint64 `json:"totalTimeSeries" required:"true"`
ActiveTimeSeries uint64 `json:"activeTimeSeries" required:"true"`
}
// MetricAttributesRequest represents the payload for the metric attributes endpoint.
type MetricAttributesRequest struct {
MetricName string `json:"metricName"`
MetricName string `json:"metricName" required:"true"`
Start *int64 `json:"start,omitempty"`
End *int64 `json:"end,omitempty"`
}
@@ -292,17 +292,17 @@ func (req *MetricAttributesRequest) UnmarshalJSON(data []byte) error {
// MetricAttribute represents a single attribute with its values and count.
type MetricAttribute struct {
Key string `json:"key"`
Values []string `json:"values"`
ValueCount uint64 `json:"valueCount"`
Key string `json:"key" required:"true"`
Values []string `json:"values" required:"true" nullable:"true"`
ValueCount uint64 `json:"valueCount" required:"true"`
}
// MetricAttributesResponse is the output structure for the metric attributes endpoint.
type MetricAttributesResponse struct {
Attributes []MetricAttribute `json:"attributes"`
TotalKeys int64 `json:"totalKeys"`
Attributes []MetricAttribute `json:"attributes" required:"true" nullable:"true"`
TotalKeys int64 `json:"totalKeys" required:"true"`
}
type MetricNameParams struct {
MetricName string `query:"metricName"`
MetricName string `query:"metricName" required:"true"`
}

View File

@@ -130,7 +130,7 @@ var (
SumType = Type{valuer.NewString("sum")}
HistogramType = Type{valuer.NewString("histogram")}
SummaryType = Type{valuer.NewString("summary")}
ExpHistogramType = Type{valuer.NewString("exponential_histogram")}
ExpHistogramType = Type{valuer.NewString("exponentialhistogram")}
UnspecifiedType = Type{valuer.NewString("")}
)