Compare commits

..

2 Commits

Author SHA1 Message Date
nityanandagohain
4d4502fa85 chore: enable ai 011y processors by default 2026-09-18 20:46:39 +05:30
Gaurav Tewari
35973efd65 feat(quick-filters): AI o11y quick filters (#12788)
#### Description

- The AI o11y explorer was reusing the traces explorer's quick filters.
It now uses its own `ai_observability` source and signal, so it loads
the gen_ai filter set.
- Values and settings keys go to
`/api/v1/ai_observability/fields/{values,keys}`, which scope suggestions
to gen_ai spans.


#### Issues closed by this PR
Close
https://github.com/orgs/SigNoz/projects/39/views/20?pane=issue&itemId=223108147&issue=SigNoz%7Cengineering-pod%7C5844

#### Screenshots / Screen Recordings


https://github.com/user-attachments/assets/f7124648-9919-46e5-9e6d-90bd72f91a50


#### Additional Information

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-18 09:49:13 +00:00
62 changed files with 586 additions and 1967 deletions

View File

@@ -3274,53 +3274,6 @@ components:
repeatVariable:
type: string
type: object
DashboardtypesAreaChartAppearance:
properties:
fillMode:
$ref: '#/components/schemas/DashboardtypesAreaFillMode'
fillOpacity:
$ref: '#/components/schemas/DashboardtypesFillOpacity'
lineInterpolation:
$ref: '#/components/schemas/DashboardtypesLineInterpolation'
lineStyle:
$ref: '#/components/schemas/DashboardtypesLineStyle'
showPoints:
type: boolean
spanGaps:
$ref: '#/components/schemas/DashboardtypesSpanGaps'
type: object
DashboardtypesAreaChartPanelSpec:
properties:
axes:
$ref: '#/components/schemas/DashboardtypesAxes'
chartAppearance:
$ref: '#/components/schemas/DashboardtypesAreaChartAppearance'
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesThresholdWithLabel'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesAreaChartVisualization'
type: object
DashboardtypesAreaChartVisualization:
properties:
fillSpans:
type: boolean
stack:
$ref: '#/components/schemas/DashboardtypesStackMode'
timePreference:
$ref: '#/components/schemas/DashboardtypesTimePreference'
type: object
DashboardtypesAreaFillMode:
enum:
- solid
- gradient
type: string
DashboardtypesAxes:
properties:
isLogScale:
@@ -3552,11 +3505,6 @@ components:
- gradient
- none
type: string
DashboardtypesFillOpacity:
maximum: 1
minimum: 0
nullable: true
type: number
DashboardtypesGettableDashboardV2:
properties:
createdAt:
@@ -4019,7 +3967,6 @@ components:
DashboardtypesPanelPlugin:
discriminator:
mapping:
signoz/AreaChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
signoz/BarChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
signoz/HistogramPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
@@ -4032,7 +3979,6 @@ components:
oneOf:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
@@ -4044,7 +3990,6 @@ components:
enum:
- signoz/TimeSeriesPanel
- signoz/BarChartPanel
- signoz/AreaChartPanel
- signoz/NumberPanel
- signoz/PieChartPanel
- signoz/TablePanel
@@ -4052,18 +3997,6 @@ components:
- signoz/ListPanel
- signoz/TextPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
properties:
kind:
enum:
- signoz/AreaChartPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesAreaChartPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
properties:
kind:
@@ -4394,12 +4327,6 @@ components:
are connected.
type: boolean
type: object
DashboardtypesStackMode:
enum:
- none
- normal
- percent
type: string
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object

View File

@@ -4099,52 +4099,6 @@ export interface DashboardGridLayoutSpecDTO {
repeatVariable?: string;
}
export enum DashboardtypesAreaFillModeDTO {
solid = 'solid',
gradient = 'gradient',
}
/**
* @minimum 0
* @maximum 1
* @nullable
*/
export type DashboardtypesFillOpacityDTO = number | null;
export enum DashboardtypesLineInterpolationDTO {
linear = 'linear',
spline = 'spline',
step_after = 'step_after',
step_before = 'step_before',
}
export enum DashboardtypesLineStyleDTO {
solid = 'solid',
dashed = 'dashed',
}
export interface DashboardtypesSpanGapsDTO {
/**
* @type string
* @description The maximum gap size to connect when fillOnlyBelow is true. Gaps larger than this duration are left disconnected.
*/
fillLessThan?: string;
/**
* @type boolean
* @description Controls whether lines connect across null values. When false (default), all gaps are connected. When true, only gaps smaller than fillLessThan are connected.
*/
fillOnlyBelow?: boolean;
}
export interface DashboardtypesAreaChartAppearanceDTO {
fillMode?: DashboardtypesAreaFillModeDTO;
fillOpacity?: DashboardtypesFillOpacityDTO | null;
lineInterpolation?: DashboardtypesLineInterpolationDTO;
lineStyle?: DashboardtypesLineStyleDTO;
/**
* @type boolean
*/
showPoints?: boolean;
spanGaps?: DashboardtypesSpanGapsDTO;
}
export interface DashboardtypesAxesDTO {
/**
* @type boolean
@@ -4222,11 +4176,6 @@ export interface DashboardtypesThresholdWithLabelDTO {
value: number;
}
export enum DashboardtypesStackModeDTO {
none = 'none',
normal = 'normal',
percent = 'percent',
}
export enum DashboardtypesTimePreferenceDTO {
global_time = 'global_time',
last_5_min = 'last_5_min',
@@ -4239,27 +4188,6 @@ export enum DashboardtypesTimePreferenceDTO {
last_1_week = 'last_1_week',
last_1_month = 'last_1_month',
}
export interface DashboardtypesAreaChartVisualizationDTO {
/**
* @type boolean
*/
fillSpans?: boolean;
stack?: DashboardtypesStackModeDTO;
timePreference?: DashboardtypesTimePreferenceDTO;
}
export interface DashboardtypesAreaChartPanelSpecDTO {
axes?: DashboardtypesAxesDTO;
chartAppearance?: DashboardtypesAreaChartAppearanceDTO;
formatting?: DashboardtypesPanelFormattingDTO;
legend?: DashboardtypesLegendDTO;
/**
* @type array,null
*/
thresholds?: DashboardtypesThresholdWithLabelDTO[] | null;
visualization?: DashboardtypesAreaChartVisualizationDTO;
}
export interface DashboardtypesBarChartVisualizationDTO {
/**
* @type boolean
@@ -4957,6 +4885,29 @@ export enum DashboardtypesFillModeDTO {
gradient = 'gradient',
none = 'none',
}
export enum DashboardtypesLineInterpolationDTO {
linear = 'linear',
spline = 'spline',
step_after = 'step_after',
step_before = 'step_before',
}
export enum DashboardtypesLineStyleDTO {
solid = 'solid',
dashed = 'dashed',
}
export interface DashboardtypesSpanGapsDTO {
/**
* @type string
* @description The maximum gap size to connect when fillOnlyBelow is true. Gaps larger than this duration are left disconnected.
*/
fillLessThan?: string;
/**
* @type boolean
* @description Controls whether lines connect across null values. When false (default), all gaps are connected. When true, only gaps smaller than fillLessThan are connected.
*/
fillOnlyBelow?: boolean;
}
export interface DashboardtypesTimeSeriesChartAppearanceDTO {
fillMode?: DashboardtypesFillModeDTO;
lineInterpolation?: DashboardtypesLineInterpolationDTO;
@@ -5009,18 +4960,6 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
spec: DashboardtypesBarChartPanelSpecDTO;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTOKind {
'signoz/AreaChartPanel' = 'signoz/AreaChartPanel',
}
export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTO {
/**
* @enum signoz/AreaChartPanel
* @type string
*/
kind: DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTOKind;
spec: DashboardtypesAreaChartPanelSpecDTO;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpecDTOKind {
'signoz/NumberPanel' = 'signoz/NumberPanel',
}
@@ -5226,7 +5165,6 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpecDTO
@@ -6151,7 +6089,6 @@ export interface DashboardtypesListableDashboardViewDTO {
export enum DashboardtypesPanelPluginKindDTO {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
'signoz/AreaChartPanel' = 'signoz/AreaChartPanel',
'signoz/NumberPanel' = 'signoz/NumberPanel',
'signoz/PieChartPanel' = 'signoz/PieChartPanel',
'signoz/TablePanel' = 'signoz/TablePanel',

View File

@@ -15,7 +15,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import CheckboxFilterHeader from './CheckboxFilterHeader';
import CheckboxValueRow from './CheckboxValueRow';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
import useActiveQueryIndex from './useActiveQueryIndex';
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
import useCheckboxDisclosure from './useCheckboxDisclosure';
import useCheckboxFilterActions from './useCheckboxFilterActions';
import useCheckboxFilterState from './useCheckboxFilterState';

View File

@@ -56,6 +56,57 @@ export function mockFieldsValuesAPI(response: {
);
}
/**
* Records every request the AI observability values endpoint receives, so a test
* can assert both the routing and the query params it was called with.
*/
export function mockAIObservabilityFieldsValuesAPI(response: {
relatedValues?: (string | null)[];
stringValues?: (string | null)[];
numberValues?: (number | null)[];
}): { requests: URLSearchParams[] } {
const requests: URLSearchParams[] = [];
server.use(
rest.get(
'http://localhost/api/v1/ai_observability/fields/values',
(req, res, ctx) => {
requests.push(req.url.searchParams);
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: response.relatedValues ?? [],
stringValues: response.stringValues ?? [],
numberValues: response.numberValues ?? [],
},
},
}),
);
},
),
);
return { requests };
}
/** Fails the test if the signal-wide values endpoint is hit at all. */
export function forbidFieldsValuesAPI(): { called: boolean } {
const state = { called: false };
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) => {
state.called = true;
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
);
return state;
}
export function mockFieldsValuesAPILoading(): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>

View File

@@ -16,7 +16,7 @@ import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { NON_SELECTED_OPERATORS } from '../checkboxFilterQuery';
import useActiveQueryIndex from '../useActiveQueryIndex';
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
import useCheckboxDisclosure from '../useCheckboxDisclosure';
import useCheckboxFilterActions from '../useCheckboxFilterActions';
import useCheckboxFilterState from '../useCheckboxFilterState';

View File

@@ -0,0 +1,81 @@
import { screen, waitFor } from '@testing-library/react';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
forbidFieldsValuesAPI,
mockAIObservabilityFieldsValuesAPI,
mockFieldsValuesAPI,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - AI observability routing', () => {
it('reads values from the AI observability endpoint and never the signal-wide one', async () => {
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
stringValues: ['openai', 'anthropic'],
});
const fieldsEndpoint = forbidFieldsValuesAPI();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.AI_OBSERVABILITY}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await expect(screen.findByText('openai')).resolves.toBeInTheDocument();
expect(screen.getByText('anthropic')).toBeInTheDocument();
expect(fieldsEndpoint.called).toBe(false);
expect(aiEndpoint.requests).toHaveLength(1);
});
it('forwards the filter key and the time range to the AI observability endpoint', async () => {
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
stringValues: ['openai'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.AI_OBSERVABILITY}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByText('openai');
const params = aiEndpoint.requests[0];
expect(params.get('name')).toBe(DEFAULT_FILTER.attributeKey.key);
expect(params.get('startUnixMilli')).toBe(
String(DEFAULT_USE_FIELD_APIS.startUnixMilli),
);
expect(params.get('endUnixMilli')).toBe(
String(DEFAULT_USE_FIELD_APIS.endUnixMilli),
);
});
it('keeps non-AI sources on the signal-wide endpoint', async () => {
mockFieldsValuesAPI({ stringValues: ['production'] });
const aiEndpoint = mockAIObservabilityFieldsValuesAPI({
stringValues: ['should-not-be-used'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await expect(screen.findByText('production')).resolves.toBeInTheDocument();
await waitFor(() => expect(aiEndpoint.requests).toHaveLength(0));
});
});

View File

@@ -1,11 +1,12 @@
import { useMemo } from 'react';
import { useGetFieldsValues } from 'api/generated/services/fields';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { FieldValuesConfig } from 'api/querySuggestions/types';
import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
import { BuilderQueryType } from 'types/api/v5/queryRange';
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
interface UseFieldValuesProps {
@@ -42,32 +43,43 @@ export function useFieldValues({
endUnixMilli,
enabled,
}: UseFieldValuesProps): UseFieldValuesReturn {
const { data, isLoading, isFetching } = useGetFieldsValues(
{
signal: filter.dataSource
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
: undefined,
name: filter.attributeKey.key,
searchText,
existingQuery,
metricNamespace,
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
startUnixMilli,
// This field does not affect the backend but I wanted to keep it here
// in case we add the support in the future
endUnixMilli,
},
{
query: {
enabled,
cacheTime: FIELD_API_CACHE_TIME,
keepPreviousData: true,
},
},
);
const isAIObservability = source === QuickFiltersSource.AI_OBSERVABILITY;
const builderQueryType: BuilderQueryType | undefined = isAIObservability
? 'builder_ai_query'
: undefined;
// The AI values endpoint is already gen_ai-scoped: no signal, no source.
const fieldValuesConfig: FieldValuesConfig = isAIObservability
? {
name: filter.attributeKey.key,
searchText,
existingQuery,
startUnixMilli,
endUnixMilli,
}
: {
signal: filter.dataSource
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
: undefined,
name: filter.attributeKey.key,
searchText,
existingQuery,
metricNamespace,
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
startUnixMilli,
// This field does not affect the backend but I wanted to keep it here
// in case we add the support in the future
endUnixMilli,
};
const {
data: values,
isLoading,
isFetching,
} = useFieldValuesSuggestion(fieldValuesConfig, builderQueryType, { enabled });
const relatedValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
@@ -78,10 +90,9 @@ export function useFieldValues({
value !== null && value !== undefined && value !== '',
) || []
);
}, [data]);
}, [values]);
const allValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
@@ -101,7 +112,7 @@ export function useFieldValues({
.map((value) => value.toString()) || [];
return [...stringValues, ...numberValues, ...boolValues];
}, [data]);
}, [values]);
return { relatedValues, allValues, isLoading, isFetching };
}

View File

@@ -1,11 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Collapse } from 'antd';
import { Undo2 } from '@signozhq/icons';
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -39,7 +39,7 @@ function Duration({
}: {
filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
source?: QuickFiltersSource;
source: QuickFiltersSource;
}): JSX.Element {
const [selectedFilters, setSelectedFilters] =
useState<
@@ -52,26 +52,11 @@ function Duration({
filter.defaultOpen ? 'durationNano' : '',
]);
const {
currentQuery,
redirectWithQueryBuilderData,
lastUsedQuery,
panelType,
} = useQueryBuilder();
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const compositeQuery = useGetCompositeQueryParam();
const isListView = panelType === PANEL_TYPES.LIST;
// In ListView mode, use index 0 for most sources; for TRACES_EXPLORER, use lastUsedQuery
// Otherwise use lastUsedQuery for non-ListView modes
const activeQueryIndex = useMemo(() => {
if (isListView) {
return source === QuickFiltersSource.TRACES_EXPLORER
? lastUsedQuery || 0
: 0;
}
return lastUsedQuery || 0;
}, [isListView, source, lastUsedQuery]);
const activeQueryIndex = useActiveQueryIndex(source);
// eslint-disable-next-line sonarjs/cognitive-complexity
const syncSelectedFilters = useMemo((): FilterType => {

View File

@@ -35,6 +35,7 @@ import { isFunction } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
import CheckboxV2 from './FilterRenderers/Checkbox/v2/CheckboxFilterV2';
import Duration from './FilterRenderers/Duration/Duration';
import Slider from './FilterRenderers/Slider/Slider';
@@ -113,14 +114,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const shouldShowDropdownInListView =
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
const activeQueryIndex = useMemo(() => {
if (isListView) {
return source === QuickFiltersSource.TRACES_EXPLORER
? lastUsedQuery || 0
: 0;
}
return lastUsedQuery || 0;
}, [isListView, source, lastUsedQuery]);
// AI observability builds a single query in the row-level views, so there is
// no query for the selector to switch between.
const isAIObservabilityRowView =
source === QuickFiltersSource.AI_OBSERVABILITY &&
(isListView || panelType === PANEL_TYPES.TRACE);
const activeQueryIndex = useActiveQueryIndex(source);
// clear all the filters for the query which is in sync with filters
const handleReset = (): void => {
@@ -167,9 +167,10 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
// In ListView, always show the 0th query's name; otherwise use the active query's name
const displayedQueryName = isListView
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
: lastQueryName;
const displayedQueryName =
isListView || isAIObservabilityRowView
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
: lastQueryName;
const handleQueryChange = (value: number): void => {
setLastUsedQuery(value);
@@ -182,7 +183,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<Typography.Text className="text">
{displayedQueryName ? 'Filters for' : 'Filters'}
</Typography.Text>
{queryOptions.length > 1 && (!isListView || shouldShowDropdownInListView) ? (
{queryOptions.length > 1 &&
!isAIObservabilityRowView &&
(!isListView || shouldShowDropdownInListView) ? (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger
placeholder="Select a query"
@@ -318,6 +321,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
return (
<Duration
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>

View File

@@ -1,12 +1,14 @@
import { useMemo } from 'react';
import { Button, Skeleton } from 'antd';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { FieldKeysConfig } from 'api/querySuggestions/types';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
import {
BuilderQueryType,
FieldContext,
FieldDataType,
TelemetryFieldKey,
@@ -41,23 +43,31 @@ function OtherFilters({
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
}): JSX.Element {
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
const { data, isFetching } = useGetFieldsKeys(
{
searchText: inputValue,
signal: signal
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
: undefined,
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
},
{ query: { enabled: !!signal } },
const builderQueryType: BuilderQueryType | undefined = isAIObservability
? 'builder_ai_query'
: undefined;
const fieldKeysConfig: FieldKeysConfig = isAIObservability
? { searchText: inputValue }
: {
searchText: inputValue,
signal: signal
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
: undefined,
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
};
const { data: fetchedKeys, isFetching } = useFieldKeysSuggestion(
fieldKeysConfig,
builderQueryType,
);
const otherFilters = useMemo<TelemetryFieldKey[]>(() => {
const rawSuggestions = Object.values(data?.data?.keys ?? {}).flat();
// Normalize: synthesize the composite `key` once so downstream reads (dedupe,
// add, render) can trust it.
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
const suggestions: TelemetryFieldKey[] = (fetchedKeys ?? []).map((attr) => ({
name: attr.name,
signal: attr.signal as TelemetryFieldKey['signal'],
fieldContext: attr.fieldContext as FieldContext,
@@ -71,7 +81,7 @@ function OtherFilters({
),
);
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
}, [data, addedFilters]);
}, [fetchedKeys, addedFilters]);
const handleAddFilter = (filter: TelemetryFieldKey): void => {
setAddedFilters((prev) => [...prev, filter]);

View File

@@ -0,0 +1,81 @@
import { screen, waitFor } from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render } from 'tests/test-utils';
import { SignalType } from '../../types';
import OtherFilters from '../OtherFilters';
const BASE_URL = ENVIRONMENT.baseURL;
const FIELDS_KEYS_URL = `${BASE_URL}/api/v1/fields/keys`;
const AI_KEYS_URL = `${BASE_URL}/api/v1/ai_observability/fields/keys`;
function keysResponse(name: string): Record<string, unknown> {
return {
status: 'success',
data: {
complete: true,
keys: {
[name]: [{ name, fieldContext: 'attribute', fieldDataType: 'string' }],
},
},
};
}
describe('OtherFilters - AI observability keys', () => {
let fieldsKeysCalled: boolean;
let aiKeysParams: URLSearchParams | undefined;
beforeEach(() => {
fieldsKeysCalled = false;
aiKeysParams = undefined;
server.use(
rest.get(FIELDS_KEYS_URL, (_, res, ctx) => {
fieldsKeysCalled = true;
return res(ctx.status(200), ctx.json(keysResponse('http.route')));
}),
rest.get(AI_KEYS_URL, (req, res, ctx) => {
aiKeysParams = req.url.searchParams;
return res(ctx.status(200), ctx.json(keysResponse('gen_ai.request.model')));
}),
);
});
function renderOtherFilters(signal: SignalType): void {
render(
<OtherFilters
signal={signal}
inputValue=""
addedFilters={[]}
setAddedFilters={jest.fn()}
/>,
);
}
it('reads AI observability keys from their own endpoint', async () => {
renderOtherFilters(SignalType.AI_OBSERVABILITY);
await expect(
screen.findByText('gen_ai.request.model'),
).resolves.toBeInTheDocument();
expect(fieldsKeysCalled).toBe(false);
});
it('does not narrow the AI keys by fieldContext', async () => {
renderOtherFilters(SignalType.AI_OBSERVABILITY);
// A `trace` context would return only the computed per-trace aggregates,
// which cannot be filtered on.
await waitFor(() => expect(aiKeysParams).toBeDefined());
expect(aiKeysParams?.get('fieldContext')).toBeNull();
});
it('keeps other signals on the signal-wide keys endpoint', async () => {
renderOtherFilters(SignalType.TRACES);
await expect(screen.findByText('http.route')).resolves.toBeInTheDocument();
await waitFor(() => expect(aiKeysParams).toBeUndefined());
});
});

View File

@@ -7,4 +7,5 @@ export const SIGNAL_DATA_SOURCE_MAP = {
[SignalType.EXCEPTIONS]: DataSource.TRACES,
[SignalType.API_MONITORING]: DataSource.TRACES,
[SignalType.METER_EXPLORER]: DataSource.METRICS,
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
};

View File

@@ -0,0 +1,81 @@
import { renderHook } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { QuickFiltersSource } from '../../types';
import useActiveQueryIndex from '../useActiveQueryIndex';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
const LAST_USED_QUERY = 2;
function mockQueryBuilder(panelType: PANEL_TYPES): void {
(useQueryBuilder as jest.Mock).mockReturnValue({
lastUsedQuery: LAST_USED_QUERY,
panelType,
});
}
describe('useActiveQueryIndex', () => {
describe('AI observability builds a single query in the row-level views', () => {
it.each([PANEL_TYPES.LIST, PANEL_TYPES.TRACE])(
'drives the first query in %s',
(panelType) => {
mockQueryBuilder(panelType);
const { result } = renderHook(() =>
useActiveQueryIndex(QuickFiltersSource.AI_OBSERVABILITY),
);
expect(result.current).toBe(0);
},
);
it.each([PANEL_TYPES.TIME_SERIES, PANEL_TYPES.TABLE])(
'follows the last used query in %s',
(panelType) => {
mockQueryBuilder(panelType);
const { result } = renderHook(() =>
useActiveQueryIndex(QuickFiltersSource.AI_OBSERVABILITY),
);
expect(result.current).toBe(LAST_USED_QUERY);
},
);
});
describe('other sources are unchanged', () => {
it('lets the traces explorer track the last used query in list view', () => {
mockQueryBuilder(PANEL_TYPES.LIST);
const { result } = renderHook(() =>
useActiveQueryIndex(QuickFiltersSource.TRACES_EXPLORER),
);
expect(result.current).toBe(LAST_USED_QUERY);
});
it('pins single-query sources to the first query in list view', () => {
mockQueryBuilder(PANEL_TYPES.LIST);
const { result } = renderHook(() =>
useActiveQueryIndex(QuickFiltersSource.INFRA_MONITORING),
);
expect(result.current).toBe(0);
});
it('tracks the last used query outside list view', () => {
mockQueryBuilder(PANEL_TYPES.TIME_SERIES);
const { result } = renderHook(() =>
useActiveQueryIndex(QuickFiltersSource.LOGS_EXPLORER),
);
expect(result.current).toBe(LAST_USED_QUERY);
});
});
});

View File

@@ -15,13 +15,21 @@ function useActiveQueryIndex(source: QuickFiltersSource): number {
const isListView = panelType === PANEL_TYPES.LIST;
return useMemo(() => {
// AI observability builds a single query in the row-level views, so its
// filters always drive the first one there.
if (source === QuickFiltersSource.AI_OBSERVABILITY) {
return isListView || panelType === PANEL_TYPES.TRACE
? 0
: lastUsedQuery || 0;
}
if (isListView) {
return source === QuickFiltersSource.TRACES_EXPLORER
? lastUsedQuery || 0
: 0;
}
return lastUsedQuery || 0;
}, [isListView, source, lastUsedQuery]);
}, [isListView, panelType, source, lastUsedQuery]);
}
export default useActiveQueryIndex;

View File

@@ -24,6 +24,7 @@ export enum SignalType {
API_MONITORING = 'api_monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
AI_OBSERVABILITY = 'ai_observability',
}
/**
@@ -69,6 +70,7 @@ export enum QuickFiltersSource {
API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
AI_OBSERVABILITY = 'ai-observability',
}
/**

View File

@@ -29,7 +29,6 @@ export const getComponentForPanelType = (
[PANEL_TYPES.LIST]:
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
[PANEL_TYPES.BAR]: Uplot,
[PANEL_TYPES.AREA]: Uplot,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.HISTOGRAM]: Uplot,
// Dashboards v2 renders this kind; nothing reaches the V1 chart map for it.

View File

@@ -336,7 +336,6 @@ export enum PANEL_TYPES {
LIST = 'list',
TRACE = 'trace',
BAR = 'bar',
AREA = 'area',
PIE = 'pie',
HISTOGRAM = 'histogram',
TEXT = 'text',

View File

@@ -109,6 +109,9 @@ export const REACT_QUERY_KEY = {
// Field Keys Suggestion Query Keys
FIELD_KEYS_SUGGESTION: 'FIELD_KEYS_SUGGESTION',
// Field Values Suggestion Query Keys
FIELD_VALUES_SUGGESTION: 'FIELD_VALUES_SUGGESTION',
// AI Assistant Query Keys
AI_ASSISTANT_EMPTY_STATE_CHIPS: 'AI_ASSISTANT_EMPTY_STATE_CHIPS',
} as const;

View File

@@ -8,6 +8,7 @@ import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { initialQueryAIWithType } from 'constants/queryBuilder';
@@ -94,6 +95,13 @@ function Explorer(): JSX.Element {
const [warning, setWarning] = useState<Warning | undefined>();
const [isOpen, setOpen] = useState<boolean>(true);
const { startUnixMilli, endUnixMilli } = useSignalFieldApis();
// existingQuery is left unset so related values auto-extract from the current query
const quickFiltersFieldApis = useMemo(
() => ({ startUnixMilli, endUnixMilli }),
[startUnixMilli, endUnixMilli],
);
const defaultQuery = useMemo(
(): Query =>
updateAllQueriesOperators(
@@ -187,8 +195,9 @@ function Explorer(): JSX.Element {
<Card className="filter" hidden={!isOpen}>
<QuickFilters
className="qf-traces-explorer"
source={QuickFiltersSource.TRACES_EXPLORER}
signal={SignalType.TRACES}
source={QuickFiltersSource.AI_OBSERVABILITY}
signal={SignalType.AI_OBSERVABILITY}
useFieldApis={quickFiltersFieldApis}
handleFilterVisibilityChange={(): void => {
setOpen(!isOpen);
}}

View File

@@ -27,7 +27,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
LIST: false,
TRACE: false,
BAR: true,
AREA: true,
PIE: false,
HISTOGRAM: false,
TEXT: false,

View File

@@ -19,6 +19,5 @@ export const PanelTypeVsPanelWrapper = {
[PANEL_TYPES.EMPTY_WIDGET]: null,
[PANEL_TYPES.PIE]: PiePanelWrapper,
[PANEL_TYPES.BAR]: BarPanel,
[PANEL_TYPES.AREA]: TimeSeriesPanel,
[PANEL_TYPES.HISTOGRAM]: HistogramPanel,
};

View File

@@ -0,0 +1,61 @@
import {
QueryKey,
useQuery,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { ErrorType } from 'api/generatedAPIInstance';
import {
RenderErrorResponseDTO,
TelemetrytypesTelemetryFieldValuesDTO,
} from 'api/generated/services/sigNoz.schemas';
import { getFieldValueSuggestions } from 'api/querySuggestions/getFieldValueSuggestions';
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
FieldValuesConfig,
FieldValuesResponse,
} from 'api/querySuggestions/types';
import { BuilderQueryType } from 'types/api/v5/queryRange';
export type FieldValuesQueryOptions = UseQueryOptions<
FieldValuesResponse,
ErrorType<RenderErrorResponseDTO>,
TelemetrytypesTelemetryFieldValuesDTO
> & { queryKey: QueryKey };
const EMPTY_FIELD_VALUES: TelemetrytypesTelemetryFieldValuesDTO = {};
export const toFieldValues = (
res: FieldValuesResponse | undefined,
): TelemetrytypesTelemetryFieldValuesDTO =>
res?.data?.values ?? EMPTY_FIELD_VALUES;
export const getFieldValuesQueryOptions = (
fieldValuesConfig: FieldValuesConfig,
builderQueryType?: BuilderQueryType,
): FieldValuesQueryOptions => ({
queryKey: [
REACT_QUERY_KEY.FIELD_VALUES_SUGGESTION,
builderQueryType,
fieldValuesConfig,
],
queryFn: ({ signal }): Promise<FieldValuesResponse> =>
getFieldValueSuggestions(fieldValuesConfig, builderQueryType, signal),
select: toFieldValues,
cacheTime: FIELD_API_CACHE_TIME,
keepPreviousData: true,
});
export const useFieldValuesSuggestion = (
fieldValuesConfig: FieldValuesConfig,
builderQueryType?: BuilderQueryType,
options?: Pick<FieldValuesQueryOptions, 'enabled'>,
): UseQueryResult<
TelemetrytypesTelemetryFieldValuesDTO,
ErrorType<RenderErrorResponseDTO>
> =>
useQuery({
...getFieldValuesQueryOptions(fieldValuesConfig, builderQueryType),
...options,
});

View File

@@ -3,7 +3,6 @@ import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { calculateWidthBasedOnStepInterval } from 'lib/uPlotV2/utils';
import uPlot, { Series } from 'uplot';
import { resolveFillOpacity, toAlphaHex } from '../utils/fillOpacity';
import { generateGradientFill } from '../utils/generateGradientFill';
import { isolatedPointFilter } from '../utils/seriesPointsFilter';
import {
@@ -59,8 +58,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<
}: {
resolvedLineColor: string;
}): Partial<Series> {
const { lineWidth, lineStyle, lineCap, fillColor, fillMode, fillOpacity } =
this.props;
const { lineWidth, lineStyle, lineCap, fillColor, fillMode } = this.props;
const lineConfig: Partial<Series> = {
stroke: resolvedLineColor,
width: lineWidth ?? DEFAULT_LINE_WIDTH,
@@ -88,17 +86,11 @@ export class UPlotSeriesBuilder extends ConfigBuilder<
} else if (this.props.drawStyle === DrawStyle.Histogram) {
lineConfig.fill = `${finalFillColor}40`;
} else if (fillMode && fillMode !== FillMode.None) {
const resolvedOpacity = resolveFillOpacity(fillOpacity);
if (fillMode === FillMode.Solid) {
lineConfig.fill = `${finalFillColor}${toAlphaHex(resolvedOpacity)}`;
lineConfig.fill = `${finalFillColor}70`;
} else if (fillMode === FillMode.Gradient) {
lineConfig.fill = (self: uPlot): CanvasGradient =>
generateGradientFill(
self,
finalFillColor,
'rgba(0, 0, 0, 0)',
resolvedOpacity,
);
generateGradientFill(self, finalFillColor, 'rgba(0, 0, 0, 0)');
}
}

View File

@@ -3,7 +3,7 @@ import uPlot from 'uplot';
import { isolatedPointFilter } from '../../utils/seriesPointsFilter';
import type { SeriesProps } from '../types';
import { DrawStyle, FillMode, LineInterpolation, LineStyle } from '../types';
import { DrawStyle, LineInterpolation, LineStyle } from '../types';
import { POINT_SIZE_FACTOR, UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
const createBaseProps = (
@@ -362,40 +362,4 @@ describe('UPlotSeriesBuilder', () => {
expect(config.points?.filter).toBeUndefined();
expect(config.points?.show).toBe(true);
});
// Pins the alpha the solid fill hardcoded before opacity was configurable.
it('fills a solid series at the default opacity', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
fillColor: '#112233',
fillMode: FillMode.Solid,
}),
);
expect(builder.getConfig().fill).toBe('#11223370');
});
it('fills a solid series at the declared opacity', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
fillColor: '#112233',
fillMode: FillMode.Solid,
fillOpacity: 0.5,
}),
);
expect(builder.getConfig().fill).toBe('#11223380');
});
it('leaves an unfilled series without a fill', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
fillColor: '#112233',
fillMode: FillMode.None,
fillOpacity: 0.5,
}),
);
expect(builder.getConfig().fill).toBeUndefined();
});
});

View File

@@ -222,8 +222,6 @@ export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
spanGaps?: boolean | number;
fillColor?: string;
fillMode?: FillMode;
/** 01, for `Solid` and `Gradient`; unset uses `DEFAULT_FILL_OPACITY`. */
fillOpacity?: number;
isDarkMode?: boolean;
stepInterval?: number;
metric?: { [key: string]: string };

View File

@@ -1,43 +0,0 @@
import {
DEFAULT_FILL_OPACITY,
GRADIENT_MID_STOP_RATIO,
resolveFillOpacity,
toAlphaHex,
} from '../fillOpacity';
describe('resolveFillOpacity', () => {
it('falls back to the default for a missing or unusable value', () => {
expect(resolveFillOpacity(undefined)).toBe(DEFAULT_FILL_OPACITY);
expect(resolveFillOpacity(null)).toBe(DEFAULT_FILL_OPACITY);
expect(resolveFillOpacity(NaN)).toBe(DEFAULT_FILL_OPACITY);
});
it('keeps 0 rather than treating it as absent', () => {
expect(resolveFillOpacity(0)).toBe(0);
});
it('clamps to 01', () => {
expect(resolveFillOpacity(-0.5)).toBe(0);
expect(resolveFillOpacity(2)).toBe(1);
});
});
describe('toAlphaHex', () => {
// The alphas hardcoded before opacity was configurable.
it('reproduces the legacy solid alpha at the default opacity', () => {
expect(toAlphaHex(DEFAULT_FILL_OPACITY)).toBe('70');
});
it('reproduces the legacy gradient mid-stop alpha at the default opacity', () => {
expect(toAlphaHex(DEFAULT_FILL_OPACITY * GRADIENT_MID_STOP_RATIO)).toBe('40');
});
it('pads a single-digit alpha', () => {
expect(toAlphaHex(0)).toBe('00');
expect(toAlphaHex(0.02)).toBe('05');
});
it('maps a full opacity to ff', () => {
expect(toAlphaHex(1)).toBe('ff');
});
});

View File

@@ -1,27 +0,0 @@
/**
* `0x70 / 255` reproduces the alpha the fill modes hardcoded before opacity was
* configurable, so a series that declares none renders byte-identically.
*/
export const DEFAULT_FILL_OPACITY = 0x70 / 255;
/** Alpha ratio between a gradient's two stops, so it keeps its falloff at any opacity. */
export const GRADIENT_MID_STOP_RATIO = 0x40 / 0x70;
/** Gradient stop offsets, top to bottom of the plot area. */
export const GRADIENT_START_STOP = 0;
export const GRADIENT_MID_STOP = 0.6;
export const GRADIENT_END_STOP = 1;
/** Clamps into 01; missing or non-finite falls back to the default. */
export function resolveFillOpacity(opacity?: number | null): number {
if (typeof opacity !== 'number' || !Number.isFinite(opacity)) {
return DEFAULT_FILL_OPACITY;
}
return Math.min(1, Math.max(0, opacity));
}
/** 01 opacity → the two-digit hex alpha suffix appended to an `#rrggbb` colour. */
export function toAlphaHex(opacity: number): string {
const alpha = Math.round(resolveFillOpacity(opacity) * 255);
return alpha.toString(16).padStart(2, '0');
}

View File

@@ -1,19 +1,9 @@
import uPlot from 'uplot';
import {
DEFAULT_FILL_OPACITY,
GRADIENT_END_STOP,
GRADIENT_MID_STOP,
GRADIENT_MID_STOP_RATIO,
GRADIENT_START_STOP,
toAlphaHex,
} from './fillOpacity';
export function generateGradientFill(
uPlotInstance: uPlot,
startColor: string,
endColor: string,
opacity: number = DEFAULT_FILL_OPACITY,
): CanvasGradient {
const g = uPlotInstance.ctx.createLinearGradient(
0,
@@ -21,11 +11,8 @@ export function generateGradientFill(
0,
uPlotInstance.bbox.height,
);
g.addColorStop(GRADIENT_START_STOP, `${startColor}${toAlphaHex(opacity)}`);
g.addColorStop(
GRADIENT_MID_STOP,
`${startColor}${toAlphaHex(opacity * GRADIENT_MID_STOP_RATIO)}`,
);
g.addColorStop(GRADIENT_END_STOP, endColor);
g.addColorStop(0, `${startColor}70`);
g.addColorStop(0.6, `${startColor}40`);
g.addColorStop(1, endColor);
return g;
}

View File

@@ -137,15 +137,6 @@ describe('stackSeries', () => {
]);
});
it('tops out at exactly 100 for values that do not divide cleanly', () => {
// Accumulating each slice's share drifts past 100 and stretches the y axis.
const data: AlignedData = [[1], [63], [78], [44]];
const [, top] = stackSeries(data, includeAll, StackMode.Percent).data;
expect(top[0]).toBe(100);
});
it('yields 0 across a column whose signed total cancels to zero', () => {
const data: AlignedData = [[1], [10], [-10]];

View File

@@ -42,11 +42,45 @@ interface BuildStackedSeriesParams {
mode: StackMode;
}
/** Per-point total. Mixed-sign columns sum signed, as "share of total" implies. */
function columnTotals({
data,
valueSeriesCount,
pointCount,
omit,
}: Omit<BuildStackedSeriesParams, 'mode'>): number[] {
const totals = Array(pointCount).fill(0) as number[];
for (let seriesIndex = 1; seriesIndex <= valueSeriesCount; seriesIndex++) {
if (omit(seriesIndex)) {
continue;
}
const rawValues = data[seriesIndex] as (number | null)[];
rawValues.forEach((rawValue, pointIndex) => {
totals[pointIndex] += rawValue == null ? 0 : Number(rawValue);
});
}
return totals;
}
/** A column whose participating series sum to 0 has no share to divide, so every slice is 0. */
function toPercent(value: number, total: number): number {
return total === 0 ? 0 : (value / total) * 100;
}
/** What a raw value adds to the stack at a given point. */
type Contribution = (value: number, pointIndex: number) => number;
function contributionForMode(params: BuildStackedSeriesParams): Contribution {
if (params.mode !== StackMode.Percent) {
return (value): number => value;
}
// Resolved up front: totals span series the accumulation below has not reached yet.
const totals = columnTotals(params);
return (value, pointIndex): number => toPercent(value, totals[pointIndex]);
}
/**
* Accumulate from last series upward: last series = raw values, first = total.
* Omitted series are copied as-is (no accumulation).
@@ -59,7 +93,14 @@ function buildStackedSeries({
mode,
}: BuildStackedSeriesParams): (number | null)[][] {
const stackedSeries: (number | null)[][] = Array(valueSeriesCount);
const columnTotals = Array(pointCount).fill(0) as number[];
const cumulativeSums = Array(pointCount).fill(0) as number[];
const contributionOf = contributionForMode({
data,
valueSeriesCount,
pointCount,
omit,
mode,
});
for (let seriesIndex = valueSeriesCount; seriesIndex >= 1; seriesIndex--) {
const rawValues = data[seriesIndex] as (number | null)[];
@@ -69,27 +110,14 @@ function buildStackedSeries({
} else {
stackedSeries[seriesIndex - 1] = rawValues.map((rawValue, pointIndex) => {
const numericValue = rawValue == null ? 0 : Number(rawValue);
return (columnTotals[pointIndex] += numericValue);
return (cumulativeSums[pointIndex] += contributionOf(
numericValue,
pointIndex,
));
});
}
}
if (mode !== StackMode.Percent) {
return stackedSeries;
}
// Scale the running totals once they are final rather than accumulating per-slice
// percentages: the topmost series then divides the total by itself and lands on
// exactly 100, where accumulated shares drift past it and stretch the y axis.
for (let seriesIndex = valueSeriesCount; seriesIndex >= 1; seriesIndex--) {
if (omit(seriesIndex)) {
continue;
}
stackedSeries[seriesIndex - 1] = stackedSeries[seriesIndex - 1].map(
(value, pointIndex) => toPercent(value as number, columnTotals[pointIndex]),
);
}
return stackedSeries;
}

View File

@@ -1,23 +0,0 @@
.row {
display: flex;
align-items: center;
gap: 12px;
}
.slider {
flex: 1;
min-width: 0;
// The design-system slider insets its track by half a thumb on each side so the
// fill follows the thumb's centre. Pull that inset back off the row so the track
// lines up with the other controls; the thumb never paints past the track edge.
margin-inline: calc(var(--slider-thumb-width, 18px) / -2);
}
.value {
flex-shrink: 0;
min-width: 36px;
text-align: right;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--l3-foreground);
}

View File

@@ -1,47 +0,0 @@
import { Slider } from '@signozhq/ui/slider';
import styles from './ConfigSlider.module.scss';
interface ConfigSliderProps {
testId: string;
value: number;
min: number;
max: number;
step: number;
/** Renders the current value beside the track (e.g. as a percentage). */
formatValue?: (value: number) => string;
onChange: (value: number) => void;
}
/**
* Numeric slider for the config sections. The design-system Slider is multi-thumb
* capable, so its callback hands back `number | number[]`; this narrows to one thumb.
*/
function ConfigSlider({
testId,
value,
min,
max,
step,
formatValue,
onChange,
}: ConfigSliderProps): JSX.Element {
return (
<div className={styles.row}>
<Slider
testId={testId}
className={styles.slider}
value={value}
min={min}
max={max}
step={step}
onChange={(next): void => onChange(Array.isArray(next) ? next[0] : next)}
/>
<span className={styles.value}>
{formatValue ? formatValue(value) : value}
</span>
</div>
);
}
export default ConfigSlider;

View File

@@ -2,16 +2,16 @@ import type { ComponentType } from 'react';
import type {
DashboardtypesLinkDTO,
DashboardtypesAxesDTO,
DashboardtypesBarChartVisualizationDTO,
DashboardtypesHistogramBucketsDTO,
DashboardtypesLegendDTO,
DashboardtypesPanelSpecDTO,
DashboardtypesTimeSeriesChartAppearanceDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
SectionKind,
type AnyThreshold,
type PanelChartAppearanceSlice,
type PanelFormattingSlice,
type PanelVisualizationSlice,
type SectionEditorProps,
type SectionSpecMap,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
@@ -94,15 +94,21 @@ export const SECTION_REGISTRY: {
},
[SectionKind.ChartAppearance]: {
Component: ChartAppearanceSection,
get: (spec): PanelChartAppearanceSlice | undefined =>
getPluginSlice<PanelChartAppearanceSlice>(spec, 'chartAppearance'),
get: (spec): DashboardtypesTimeSeriesChartAppearanceDTO | undefined =>
getPluginSlice<DashboardtypesTimeSeriesChartAppearanceDTO>(
spec,
'chartAppearance',
),
update: (spec, chartAppearance): PanelSpec =>
updatePluginSlice(spec, 'chartAppearance', chartAppearance),
},
[SectionKind.Visualization]: {
Component: VisualizationSection,
get: (spec): PanelVisualizationSlice | undefined =>
getPluginSlice<PanelVisualizationSlice>(spec, 'visualization'),
get: (spec): DashboardtypesBarChartVisualizationDTO | undefined =>
getPluginSlice<DashboardtypesBarChartVisualizationDTO>(
spec,
'visualization',
),
update: (spec, visualization): PanelSpec =>
updatePluginSlice(spec, 'visualization', visualization),
},

View File

@@ -9,11 +9,8 @@ import type {
SectionKind,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import { resolveFillOpacity } from 'lib/uPlotV2/utils/fillOpacity';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
import ConfigSlider from '../../controls/ConfigSlider/ConfigSlider';
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
import { SegmentIcon } from '../../controls/segmentIcons';
import type { SectionEditorContext } from '../../sectionContext';
@@ -75,21 +72,10 @@ const FILL_MODE_OPTIONS = [
},
];
// An always-filled kind's wire enum (`AreaFillMode`) has no `none`.
const FILLED_FILL_MODE_OPTIONS = FILL_MODE_OPTIONS.filter(
(option) => option.value !== DashboardtypesFillModeDTO.none,
);
const FILL_OPACITY_STEP = 0.01;
function formatOpacity(opacity: number): string {
return `${Math.round(opacity * 100)}%`;
}
/**
* Edits the `chartAppearance` slice of a TimeSeries panel spec: line style /
* interpolation, fill mode, fill opacity, point markers, and the connect-null-gaps
* threshold. Each control is gated by its `controls` flag.
* interpolation, fill mode, point markers, and the connect-null-gaps threshold. Each
* control is gated by its `controls` flag.
*/
function ChartAppearanceSection({
value,
@@ -138,9 +124,7 @@ function ChartAppearanceSection({
<ConfigSegmented
testId="panel-editor-v2-fill-mode"
value={value?.fillMode}
items={
controls.fillOpacity ? FILLED_FILL_MODE_OPTIONS : FILL_MODE_OPTIONS
}
items={FILL_MODE_OPTIONS}
onChange={(next): void =>
onChange({ ...value, fillMode: next as DashboardtypesFillModeDTO })
}
@@ -148,22 +132,6 @@ function ChartAppearanceSection({
</div>
)}
{controls.fillOpacity && (
<div className={styles.field}>
<Typography.Text>Fill opacity</Typography.Text>
<ConfigSlider
testId="panel-editor-v2-fill-opacity"
// The chart's own default, so the thumb starts where an unset fill renders.
value={resolveFillOpacity(value?.fillOpacity)}
min={0}
max={1}
step={FILL_OPACITY_STEP}
formatValue={formatOpacity}
onChange={(fillOpacity): void => onChange({ ...value, fillOpacity })}
/>
</div>
)}
{controls.showPoints && (
<ConfigSwitch
testId="panel-editor-v2-show-points"

View File

@@ -5,7 +5,6 @@ import {
DashboardtypesLineStyleDTO,
type DashboardtypesTimeSeriesChartAppearanceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { DEFAULT_FILL_OPACITY } from 'lib/uPlotV2/utils/fillOpacity';
import ChartAppearanceSection from '../ChartAppearanceSection';
@@ -105,66 +104,6 @@ describe('ChartAppearanceSection', () => {
});
});
it('shows the fill opacity at the chart default when the spec omits it', () => {
render(
<ChartAppearanceSection
value={undefined}
controls={{ fillOpacity: true }}
onChange={jest.fn()}
/>,
);
expect(
screen.getByTestId('panel-editor-v2-fill-opacity'),
).toBeInTheDocument();
expect(
screen.getByText(`${Math.round(DEFAULT_FILL_OPACITY * 100)}%`),
).toBeInTheDocument();
});
it('renders the stored fill opacity as a percentage', () => {
render(
<ChartAppearanceSection
value={{ fillOpacity: 0.25 }}
controls={{ fillOpacity: true }}
onChange={jest.fn()}
/>,
);
expect(screen.getByText('25%')).toBeInTheDocument();
});
it('offers no None fill mode to a kind that declares fill opacity', () => {
render(
<ChartAppearanceSection
value={undefined}
controls={{ fillMode: true, fillOpacity: true }}
onChange={jest.fn()}
/>,
);
expect(screen.getByText('Solid')).toBeInTheDocument();
expect(screen.getByText('Gradient')).toBeInTheDocument();
expect(screen.queryByText('None')).not.toBeInTheDocument();
expect(
screen.getByTestId('panel-editor-v2-fill-opacity'),
).toBeInTheDocument();
});
it('offers all three fill modes to a kind that can be unfilled', () => {
render(
<ChartAppearanceSection
value={undefined}
controls={{ fillMode: true }}
onChange={jest.fn()}
/>,
);
expect(screen.getByText('None')).toBeInTheDocument();
expect(screen.getByText('Solid')).toBeInTheDocument();
expect(screen.getByText('Gradient')).toBeInTheDocument();
});
it('writes the chosen line interpolation through the dropdown', async () => {
const onChange = jest.fn();
render(

View File

@@ -1,17 +1,14 @@
import { Typography } from '@signozhq/ui/typography';
import type { DashboardtypesStackModeDTO } from 'api/generated/services/sigNoz.schemas';
import type {
SectionEditorProps,
SectionKind,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import { EQueryType } from 'types/common/dashboard';
import ConfigSegmented from '../../controls/ConfigSegmented/ConfigSegmented';
import ConfigSelect from '../../controls/ConfigSelect/ConfigSelect';
import ConfigSwitch from '../../controls/ConfigSwitch/ConfigSwitch';
import PanelTypeSwitcher from '../../PanelTypeSwitcher/PanelTypeSwitcher';
import type { SectionEditorContext } from '../../sectionContext';
import { STACK_MODE_OPTIONS } from './stackModeOptions';
import { TIME_PREFERENCE_OPTIONS } from './timePreferenceOptions';
import styles from './VisualizationSection.module.scss';
@@ -24,10 +21,9 @@ type VisualizationSectionProps = SectionEditorProps<SectionKind.Visualization> &
/**
* Edits the `visualization` slice: the panel-type switcher (`switchPanelKind`, every
* kind), the per-panel time preference, bar stacking (`stackedBarChart`, Bar only),
* area stacking (`stack`, Area only) and gap filling (`fillSpans`). Each control is
* gated by its `controls` flag, so a kind only renders — and only writes — the fields
* its spec supports.
* kind), the per-panel time preference, bar stacking (`stackedBarChart`, Bar only), and
* gap filling (`fillSpans`, TimeSeries only). Each control is gated by its `controls`
* flag, so a kind only renders — and only writes — the fields its spec supports.
*/
function VisualizationSection({
value,
@@ -81,20 +77,6 @@ function VisualizationSection({
/>
)}
{controls.stackMode && (
<div className={styles.field}>
<Typography.Text>Stack series</Typography.Text>
<ConfigSegmented
testId="panel-editor-v2-stack-mode"
value={value?.stack}
items={STACK_MODE_OPTIONS}
onChange={(next): void =>
onChange({ ...value, stack: next as DashboardtypesStackModeDTO })
}
/>
</div>
)}
{controls.fillSpans && (
<ConfigSwitch
testId="panel-editor-v2-fill-spans"

View File

@@ -1,9 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
DashboardtypesStackModeDTO,
DashboardtypesTimePreferenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { DashboardtypesTimePreferenceDTO } from 'api/generated/services/sigNoz.schemas';
import VisualizationSection from '../VisualizationSection';
@@ -118,43 +115,6 @@ describe('VisualizationSection', () => {
});
});
it('writes the chosen stack mode through the segmented control', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
render(
<VisualizationSection
value={{ fillSpans: true }}
controls={{ switchPanelKind: true, stackMode: true }}
onChange={onChange}
/>,
);
expect(screen.getByTestId('panel-editor-v2-stack-mode')).toBeInTheDocument();
await user.click(screen.getByText('Percent'));
expect(onChange).toHaveBeenCalledWith({
fillSpans: true,
stack: DashboardtypesStackModeDTO.percent,
});
});
it('renders no stack-mode control for a kind that declares bar stacking', () => {
render(
<VisualizationSection
value={undefined}
controls={{ switchPanelKind: true, stacking: true }}
onChange={jest.fn()}
/>,
);
expect(
screen.getByTestId('panel-editor-v2-stacked-bar-chart'),
).toBeInTheDocument();
expect(
screen.queryByTestId('panel-editor-v2-stack-mode'),
).not.toBeInTheDocument();
});
it('toggles fill spans through onChange', () => {
const onChange = jest.fn();
render(

View File

@@ -1,10 +0,0 @@
import { DashboardtypesStackModeDTO } from 'api/generated/services/sigNoz.schemas';
import type { ConfigSegmentedItem } from '../../controls/ConfigSegmented/ConfigSegmented';
// `percent` rescales each x-slice to its column total; the y axis follows.
export const STACK_MODE_OPTIONS: ConfigSegmentedItem[] = [
{ value: DashboardtypesStackModeDTO.none, label: 'None' },
{ value: DashboardtypesStackModeDTO.normal, label: 'Normal' },
{ value: DashboardtypesStackModeDTO.percent, label: 'Percent' },
];

View File

@@ -27,7 +27,6 @@ const { time_series, scalar, raw } = Querybuildertypesv5RequestTypeDTO;
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/BarChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/AreaChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/NumberPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/HistogramPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/PieChartPanel': [QUERY_BUILDER, CLICKHOUSE],
@@ -40,7 +39,6 @@ const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
const EXPECTED_SIGNALS: Record<PanelKind, TelemetrytypesSignalDTO[]> = {
'signoz/TimeSeriesPanel': [metrics, logs, traces],
'signoz/BarChartPanel': [metrics, logs, traces],
'signoz/AreaChartPanel': [metrics, logs, traces],
'signoz/NumberPanel': [metrics, logs, traces],
'signoz/HistogramPanel': [metrics, logs, traces],
'signoz/PieChartPanel': [metrics, logs, traces],
@@ -72,13 +70,6 @@ const EXPECTED_QUERY_CAPABILITIES: Partial<
orderTiebreaker: false,
serverPaginated: false,
},
'signoz/AreaChartPanel': {
requestType: time_series,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
},
'signoz/HistogramPanel': {
requestType: time_series,
formatTableResultForUI: false,

View File

@@ -1,233 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import type { DashboardtypesAreaChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import TimeSeries from 'lib/visualization/charts/TimeSeries/TimeSeries';
import ChartManager from 'lib/visualization/components/ChartManager/ChartManager';
import TooltipFooter from 'lib/visualization/panels/components/TooltipFooter';
import { PanelMode } from 'lib/visualization/panels/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { IRenderTooltipFooterArgs } from 'lib/uPlotV2/components/types';
import {
flattenTimeSeries,
getExecStats,
getTimeSeriesResults,
} from 'pages/DashboardPage/DashboardContainer/queryV5/v5ResponseData';
import { prepareAlignedData } from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
import { useTimezone } from 'providers/Timezone';
import NoData from '../../components/NoData/NoData';
import { useGroupByPerQuery } from '../../hooks/useGroupByPerQuery';
import PanelStyles from '../../panel.module.scss';
import { PanelRendererProps } from '../../types/rendererProps';
import {
resolveDecimalPrecision,
resolveLegendPosition,
resolveStackMode,
} from '../../utils/chartAppearance/resolvers';
import { stepClickTimeRange } from '../../utils/drilldown/chartClickTimeRange';
import { enrichChartClick } from '../../utils/drilldown/enrichChartClick';
import { getBuilderQueries } from '../../utils/getBuilderQueries';
import { getPanelTimeRange } from '../../utils/getPanelTimeRange';
import { sortSeriesByMeanDesc } from '../../utils/sortSeriesByMean';
import { buildAreaChartConfig } from './utils/buildConfig';
import { ChartClickData } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
function AreaChartPanelRenderer({
panelId,
panel,
data,
isFetching,
refetch,
onClick,
onDragSelect,
dashboardPreference,
panelMode,
onCloseStandaloneView,
enableDrillDown,
}: PanelRendererProps<'signoz/AreaChartPanel'>): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
const spec = useMemo<DashboardtypesAreaChartPanelSpecDTO>(
() => panel.spec.plugin.spec,
[panel.spec.plugin.spec],
);
const builderQueries = useMemo(
() => getBuilderQueries(panel.spec.queries),
[panel.spec.queries],
);
// X-scale clamps come from the request that produced the data, so each panel
// pins to the window it fetched — matters during drag-zoom transitions before
// new data arrives.
const { minTimeScale, maxTimeScale } = useMemo(() => {
const { startTime, endTime } = getPanelTimeRange(data.requestPayload);
return { minTimeScale: startTime, maxTimeScale: endTime };
}, [data.requestPayload]);
const groupByPerQuery = useGroupByPerQuery(builderQueries);
const flatSeries = useMemo(
() =>
sortSeriesByMeanDesc(
flattenTimeSeries(
getTimeSeriesResults(data.response),
data.legendMap ?? {},
),
),
[data.response, data.legendMap],
);
const config = useMemo(
() =>
buildAreaChartConfig({
panelId,
spec,
builderQueries,
series: flatSeries,
stepIntervals: getExecStats(data.response)?.stepIntervals,
isDarkMode,
timezone,
panelMode,
minTimeScale,
maxTimeScale,
onDragSelect,
}),
[
panelId,
spec,
builderQueries,
flatSeries,
data.response,
isDarkMode,
timezone,
panelMode,
minTimeScale,
maxTimeScale,
onDragSelect,
// TooltipPlugin mutates `config` for cursor sync; rebuild on syncMode change
// so a fresh instance doesn't inherit stale sync settings (e.g. "No Sync").
dashboardPreference?.syncMode,
],
);
const chartData = useMemo(() => prepareAlignedData(flatSeries), [flatSeries]);
const decimalPrecision = useMemo(
() => resolveDecimalPrecision(spec.formatting?.decimalPrecision),
[spec.formatting?.decimalPrecision],
);
const legendPosition = useMemo(() => {
return resolveLegendPosition(spec.legend?.position);
}, [spec.legend?.position]);
// The standalone View modal shows V1's graph-manager legend below the chart:
// Filter Series + per-series show/hide + Save. Series visibility auto-persists to
// localStorage (STANDALONE_VIEW selection prefs), keyed by panelId.
const layoutChildren = useMemo(
() =>
panelMode === PanelMode.STANDALONE_VIEW ? (
<div className={PanelStyles.chartManagerContainer}>
<ChartManager
config={config}
alignedData={chartData}
yAxisUnit={spec.formatting?.unit}
decimalPrecision={decimalPrecision}
onCancel={onCloseStandaloneView}
/>
</div>
) : null,
[
panelMode,
config,
chartData,
spec.formatting?.unit,
decimalPrecision,
onCloseStandaloneView,
],
);
const renderTooltipFooter = useCallback(
({ isPinned, dismiss }: IRenderTooltipFooterArgs) => (
<TooltipFooter
id={panelId}
isPinned={isPinned}
canDrilldown={!!enableDrillDown}
dismiss={dismiss}
/>
),
[panelId, enableDrillDown],
);
// Keying on sync prefs forces a full chart teardown/re-mount so stale sync
// settings aren't inherited — the only way to fully reset the uPlot instance.
const key = `${dashboardPreference?.syncMode}-${dashboardPreference?.syncFilterMode}`;
const handleChartClick = useCallback(
(args: ChartClickData): void => {
if (!onClick) {
return;
}
const payload = enrichChartClick({
clickData: args,
series: flatSeries,
builderQueries,
});
if (!payload) {
return;
}
const timeRange = stepClickTimeRange({
clickedDataTimestamp: args.clickedDataTimestamp,
queryName: payload.context.queryName,
builderQueries,
stepInterval: getExecStats(data.response)?.stepIntervals?.[
payload.context.queryName
],
});
onClick({ ...payload, context: { ...payload.context, timeRange } });
},
[onClick, flatSeries, builderQueries, data.response],
);
return (
<div
ref={graphRef}
data-testid="area-chart-renderer"
className={PanelStyles.panelContainer}
>
{flatSeries.length === 0 && (
<NoData isFetching={isFetching} onRetry={refetch} panel={panel} />
)}
{flatSeries.length > 0 &&
containerDimensions.width > 0 &&
containerDimensions.height > 0 && (
<TimeSeries
key={key}
config={config}
data={chartData}
legendConfig={{ position: legendPosition }}
layoutChildren={layoutChildren}
groupByPerQuery={groupByPerQuery}
canPinTooltip
timezone={timezone}
yAxisUnit={spec.formatting?.unit}
decimalPrecision={decimalPrecision}
width={containerDimensions.width}
height={containerDimensions.height}
syncMode={dashboardPreference?.syncMode}
syncFilterMode={dashboardPreference?.syncFilterMode}
stack={resolveStackMode(spec.visualization?.stack)}
renderTooltipFooter={renderTooltipFooter}
onClick={enableDrillDown ? handleChartClick : undefined}
/>
)}
</div>
);
}
export default AreaChartPanelRenderer;

View File

@@ -1,48 +0,0 @@
import { ChartArea } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/AreaChartPanel'> = {
kind: 'signoz/AreaChartPanel',
displayName: 'Area Chart',
mode: 'query',
icon: ChartArea,
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedSignals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
supportedQueryTypes: [
EQueryType.QUERY_BUILDER,
EQueryType.CLICKHOUSE,
EQueryType.PROM,
],
queryBuilderFields: {},
queryCapabilities: {
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
},
actions: {
view: true,
edit: true,
clone: true,
download: { csv: false, png: true, svg: true },
createAlert: true,
search: false,
drilldown: true,
},
};

View File

@@ -1,42 +0,0 @@
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
import {
SectionKind,
ThresholdVariant,
type SectionConfig,
} from '../../types/sections';
// Declaring `fillOpacity` also makes the kind always-filled: `fillMode` drops `none`
// and defaults to solid.
export const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: {
switchPanelKind: true,
timePreference: true,
stackMode: true,
fillSpans: true,
},
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{
kind: SectionKind.Legend,
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
},
{
kind: SectionKind.ChartAppearance,
controls: {
lineStyle: true,
lineInterpolation: true,
fillMode: true,
fillOpacity: true,
showPoints: true,
spanGaps: true,
},
},
{
kind: SectionKind.Thresholds,
controls: { variant: ThresholdVariant.LABEL },
},
{ kind: SectionKind.ContextLinks },
];

View File

@@ -1,160 +0,0 @@
import type { DashboardtypesAreaChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import {
buildBaseConfig,
minStepInterval,
type TimeAxisChromeArgs,
} from 'pages/DashboardPage/DashboardContainer/Panels/utils/baseConfigBuilder';
import {
LINE_INTERPOLATION_MAP,
LINE_STYLE_MAP,
} from 'pages/DashboardPage/DashboardContainer/Panels/utils/chartAppearance/enumMaps';
import {
resolveAreaFillMode,
resolveSpanGaps,
} from 'pages/DashboardPage/DashboardContainer/Panels/utils/chartAppearance/resolvers';
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
import type { PanelSeries } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
import {
hasSingleVisiblePoint,
toClickPluginPayload,
} from 'pages/DashboardPage/DashboardContainer/queryV5/uplotData';
import getLabelName from 'lib/getLabelName';
import {
DrawStyle,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import type { BuilderQuery } from 'types/api/v5/queryRange';
const DEFAULT_POINT_SIZE = 5;
export interface BuildAreaChartConfigArgs extends TimeAxisChromeArgs {
spec: DashboardtypesAreaChartPanelSpecDTO;
/** Flat list of builder queries (see `getBuilderQueries`); powers per-query legend resolution. */
builderQueries: BuilderQuery[];
/** Flattened V5 series (see `flattenTimeSeries`). */
series: PanelSeries[];
}
/**
* Builds a `UPlotConfigBuilder` for an Area panel: shared scaffolding plus one filled
* series per result. Stacking is declared on the chart component instead, which hands
* it to the builder.
*/
export function buildAreaChartConfig({
panelId,
spec,
builderQueries,
series,
stepIntervals,
isDarkMode,
timezone,
panelMode,
onDragSelect,
onClick,
minTimeScale,
maxTimeScale,
}: BuildAreaChartConfigArgs): UPlotConfigBuilder {
const builder = buildBaseConfig({
panelId,
isTimeAxis: true,
isDarkMode,
timezone,
panelMode,
isLogScale: spec.axes?.isLogScale,
softMin: spec.axes?.softMin ?? undefined,
softMax: spec.axes?.softMax ?? undefined,
formatting: spec.formatting,
thresholds: spec.thresholds,
stepIntervals,
clickPayload: toClickPluginPayload(series),
minTimeScale,
maxTimeScale,
onDragSelect,
onClick,
});
addSeries({
builder,
spec,
builderQueries,
series,
stepIntervals,
isDarkMode,
});
return builder;
}
interface AddSeriesArgs {
builder: UPlotConfigBuilder;
spec: DashboardtypesAreaChartPanelSpecDTO;
builderQueries: BuilderQuery[];
series: PanelSeries[];
/** Per-query step intervals (seconds); floor for a numeric spanGaps threshold. */
stepIntervals?: Record<string, number>;
isDarkMode: boolean;
}
/**
* Adds one filled uPlot series per flattened V5 series; mutates the builder in place.
* Order must match `prepareAlignedData` — both iterate the same flat list.
*/
function addSeries({
builder,
spec,
builderQueries,
series,
stepIntervals,
isDarkMode,
}: AddSeriesArgs): void {
const chartAppearance = spec.chartAppearance;
// `customColors` is nullable on the spec; coerce so `addSeries` always gets
// a defined record (it dereferences keys without a guard).
const colorMapping = spec.legend?.customColors ?? {};
const resolvedSpanGaps = chartAppearance?.spanGaps
? resolveSpanGaps(chartAppearance.spanGaps)
: true;
// A numeric spanGaps is a max-gap threshold (seconds); floor it at the step interval so a
// sub-step value doesn't break the line at every normal point. Boolean `true` passes through.
const minStep = stepIntervals ? minStepInterval(stepIntervals) : undefined;
const spanGaps =
typeof resolvedSpanGaps === 'number' && minStep !== undefined
? Math.max(minStep, resolvedSpanGaps)
: resolvedSpanGaps;
const lineStyle = chartAppearance?.lineStyle
? LINE_STYLE_MAP[chartAppearance.lineStyle]
: LineStyle.Solid;
const lineInterpolation = chartAppearance?.lineInterpolation
? LINE_INTERPOLATION_MAP[chartAppearance.lineInterpolation]
: LineInterpolation.Spline;
const fillMode = resolveAreaFillMode(chartAppearance?.fillMode);
// Null and undefined both mean "kind default", which the chart layer resolves.
const fillOpacity = chartAppearance?.fillOpacity ?? undefined;
series.forEach((s) => {
const hasSingleValidPoint = hasSingleVisiblePoint(s.values);
const baseLabel = getLabelName(s.labels, s.queryName, s.legend);
const label = resolveSeriesLabelV5(s, builderQueries, baseLabel);
builder.addSeries({
scaleKey: 'y',
// A single visible point can't be drawn as a line — degrade to points
// so the user still sees the datum (matches V1 behavior).
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
label,
colorMapping,
spanGaps,
lineStyle,
lineInterpolation,
showPoints: chartAppearance?.showPoints || hasSingleValidPoint,
pointSize: DEFAULT_POINT_SIZE,
fillMode,
fillOpacity,
isDarkMode,
metric: s.labels,
});
});
}

View File

@@ -1,4 +1,3 @@
import { definition as AreaChart } from './kinds/AreaChartPanel/definition';
import { definition as BarChart } from './kinds/BarChartPanel/definition';
import { definition as Histogram } from './kinds/HistogramPanel/definition';
import { definition as NumberValue } from './kinds/NumberPanel/definition';
@@ -22,7 +21,6 @@ export const PANELS: PanelRegistry = {
[NumberValue.kind]: NumberValue,
[Table.kind]: Table,
[BarChart.kind]: BarChart,
[AreaChart.kind]: AreaChart,
[PieChart.kind]: PieChart,
[Histogram.kind]: Histogram,
[List.kind]: List,

View File

@@ -28,11 +28,6 @@ export type PanelInteractionMap = Record<PanelKind, object> & {
onDragSelect?: DragSelect;
onCloseStandaloneView?: CloseStandaloneView;
};
'signoz/AreaChartPanel': {
onClick?: (event: DrilldownClickPayload) => void;
onDragSelect?: DragSelect;
onCloseStandaloneView?: CloseStandaloneView;
};
'signoz/TablePanel': { onClick?: (event: DrilldownClickPayload) => void };
'signoz/PieChartPanel': { onClick?: (event: DrilldownClickPayload) => void };
'signoz/NumberPanel': { onClick?: (event: DrilldownClickPayload) => void };

View File

@@ -18,7 +18,6 @@ export type PanelKind = `${DashboardtypesPanelPluginKindDTO}`;
export const PANEL_KIND_TO_PANEL_TYPE: Record<PanelKind, PANEL_TYPES> = {
'signoz/TimeSeriesPanel': PANEL_TYPES.TIME_SERIES,
'signoz/BarChartPanel': PANEL_TYPES.BAR,
'signoz/AreaChartPanel': PANEL_TYPES.AREA,
'signoz/NumberPanel': PANEL_TYPES.VALUE,
'signoz/PieChartPanel': PANEL_TYPES.PIE,
'signoz/TablePanel': PANEL_TYPES.TABLE,

View File

@@ -1,23 +1,18 @@
import type {
DashboardtypesLinkDTO,
DashboardtypesAxesDTO,
DashboardtypesBarChartVisualizationDTO,
DashboardtypesComparisonThresholdDTO,
DashboardtypesFillModeDTO,
DashboardtypesFillOpacityDTO,
DashboardtypesHeaderOptionsDTO,
DashboardtypesHistogramBucketsDTO,
DashboardtypesLegendDTO,
DashboardtypesLineInterpolationDTO,
DashboardtypesLineStyleDTO,
DashboardtypesPanelFormattingDTO,
DashboardtypesPanelSpecDTO,
DashboardtypesSpanGapsDTO,
DashboardtypesStackModeDTO,
DashboardtypesTableFormattingDTO,
DashboardtypesTableThresholdDTO,
DashboardtypesTextPresentationDTO,
DashboardtypesThresholdWithLabelDTO,
DashboardtypesTimePreferenceDTO,
DashboardtypesTimeSeriesChartAppearanceDTO,
TelemetrytypesTelemetryFieldKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { LegendSeriesResolver } from '../utils/legendSeries';
@@ -92,36 +87,15 @@ export type AnyThreshold =
export type PanelFormattingSlice = DashboardtypesPanelFormattingDTO &
Pick<DashboardtypesTableFormattingDTO, 'columnUnits'>;
/** Superset spanning every kind's chart-appearance DTO. */
export interface PanelChartAppearanceSlice {
lineStyle?: DashboardtypesLineStyleDTO;
lineInterpolation?: DashboardtypesLineInterpolationDTO;
/** Area's wire enum is a nominally distinct subset with the same members. */
fillMode?: DashboardtypesFillModeDTO;
fillOpacity?: DashboardtypesFillOpacityDTO;
showPoints?: boolean;
spanGaps?: DashboardtypesSpanGapsDTO;
}
/** Superset spanning every kind's visualization DTO. */
export interface PanelVisualizationSlice {
timePreference?: DashboardtypesTimePreferenceDTO;
/** Bar stacking; a kind declares this or `stack`, never both. */
stackedBarChart?: boolean;
/** Area stacking. */
stack?: DashboardtypesStackModeDTO;
fillSpans?: boolean;
}
export interface SectionSpecMap {
[SectionKind.Formatting]: PanelFormattingSlice; // spec.plugin.spec.formatting
[SectionKind.Axes]: DashboardtypesAxesDTO; // spec.plugin.spec.axes
[SectionKind.Legend]: DashboardtypesLegendDTO; // spec.plugin.spec.legend
[SectionKind.ChartAppearance]: PanelChartAppearanceSlice; // spec.plugin.spec.chartAppearance
[SectionKind.ChartAppearance]: DashboardtypesTimeSeriesChartAppearanceDTO; // spec.plugin.spec.chartAppearance
[SectionKind.Buckets]: DashboardtypesHistogramBucketsDTO; // spec.plugin.spec.histogramBuckets
// spec.plugin.spec.visualization — typed as the superset of every kind's shape;
// spec.plugin.spec.visualization — typed as the Bar shape (widest superset);
// the `controls` bag gates which fields each kind writes.
[SectionKind.Visualization]: PanelVisualizationSlice;
[SectionKind.Visualization]: DashboardtypesBarChartVisualizationDTO;
[SectionKind.Thresholds]: AnyThreshold[]; // spec.plugin.spec.thresholds (variant picks the editor)
[SectionKind.ContextLinks]: DashboardtypesLinkDTO[]; // spec.links (PANEL-level)
[SectionKind.Columns]: TelemetrytypesTelemetryFieldKeyDTO[]; // spec.plugin.spec.selectFields (List)
@@ -150,11 +124,6 @@ export interface SectionControls {
lineStyle?: boolean;
lineInterpolation?: boolean;
fillMode?: boolean;
/**
* Declaring it also marks the kind always-filled: `fillMode` drops `none` to match
* the narrower `AreaFillMode` wire enum the save API validates against.
*/
fillOpacity?: boolean;
showPoints?: boolean;
spanGaps?: boolean;
};
@@ -164,13 +133,12 @@ export interface SectionControls {
mergeQueries?: boolean;
};
// switchPanelKind → the visualization-type switcher (every kind, so you can switch
// away from any panel); stacking → stackedBarChart (Bar); stackMode → stack
// (Area); fillSpans → fill gaps with 0 (TimeSeries / Area).
// away from any panel); stacking → stackedBarChart (Bar); fillSpans → fill gaps with
// 0 (TimeSeries).
[SectionKind.Visualization]: {
switchPanelKind: boolean;
timePreference?: boolean;
stacking?: boolean;
stackMode?: boolean;
fillSpans?: boolean;
};
// Editor discriminator (not a spec field): which threshold variant a kind edits.

View File

@@ -5,14 +5,12 @@ import {
DashboardtypesLineInterpolationDTO,
DashboardtypesLineStyleDTO,
type DashboardtypesPanelSpecDTO,
DashboardtypesStackModeDTO,
DashboardtypesThresholdFormatDTO,
DashboardtypesTimePreferenceDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { defaultColumnsForSignal } from '../../../PanelEditor/ListColumnsEditor/selectFields';
import { sections as areaSections } from '../../kinds/AreaChartPanel/sections';
import { sections as listSections } from '../../kinds/ListPanel/sections';
import { sections as timeSeriesSections } from '../../kinds/TimeSeriesPanel/sections';
import {
@@ -180,89 +178,6 @@ describe('buildPluginSpec', () => {
});
});
it('translates Bar stacking into an Area stack mode', () => {
const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, stackMode: true },
},
];
const oldSpec = oldSpecWith({ visualization: { stackedBarChart: true } });
expect(buildPluginSpec(sections, { oldSpec }).visualization).toStrictEqual({
stack: DashboardtypesStackModeDTO.normal,
});
});
it('translates an Area stack mode into Bar stacking, collapsing percent', () => {
const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, stacking: true },
},
];
const fromPercent = oldSpecWith({
visualization: { stack: DashboardtypesStackModeDTO.percent },
});
expect(
buildPluginSpec(sections, { oldSpec: fromPercent }).visualization,
).toStrictEqual({ stackedBarChart: true });
const fromNone = oldSpecWith({
visualization: { stack: DashboardtypesStackModeDTO.none },
});
expect(
buildPluginSpec(sections, { oldSpec: fromNone }).visualization,
).toStrictEqual({ stackedBarChart: false });
});
it('defaults a stack-mode kind to normal when there is nothing to carry', () => {
const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, stackMode: true },
},
];
expect(buildPluginSpec(sections).visualization).toStrictEqual({
stack: DashboardtypesStackModeDTO.normal,
});
expect(
buildPluginSpec(sections, { oldSpec: oldSpecWith({}) }).visualization,
).toStrictEqual({ stack: DashboardtypesStackModeDTO.normal });
});
it('carries an Area stack mode unchanged between Area panels', () => {
const sections: SectionConfig[] = [
{
kind: SectionKind.Visualization,
controls: { switchPanelKind: true, stackMode: true },
},
];
const oldSpec = oldSpecWith({
visualization: { stack: DashboardtypesStackModeDTO.percent },
});
expect(buildPluginSpec(sections, { oldSpec }).visualization).toStrictEqual({
stack: DashboardtypesStackModeDTO.percent,
});
});
it('seeds no stacking field when the target declares neither control', () => {
const sections: SectionConfig[] = [
{ kind: SectionKind.Visualization, controls: { switchPanelKind: true } },
];
const oldSpec = oldSpecWith({
visualization: {
stackedBarChart: true,
stack: DashboardtypesStackModeDTO.percent,
},
});
expect(buildPluginSpec(sections, { oldSpec })).toStrictEqual({});
});
it('carries old legend position but never customColors', () => {
const sections: SectionConfig[] = [
{
@@ -349,99 +264,6 @@ describe('buildPluginSpec', () => {
expect(buildPluginSpec(sections)).toStrictEqual({});
});
it('defaults fillMode to solid for a kind that offers fill opacity', () => {
const sections: SectionConfig[] = [
{
kind: SectionKind.ChartAppearance,
controls: { fillMode: true, fillOpacity: true },
},
];
expect(buildPluginSpec(sections).chartAppearance).toStrictEqual({
fillMode: DashboardtypesFillModeDTO.solid,
});
});
// `none` is absent from the AreaFillMode wire enum, so carrying it would fail the save.
it('coerces an unfilled source fillMode to solid for a filled kind', () => {
const sections: SectionConfig[] = [
{
kind: SectionKind.ChartAppearance,
controls: { fillMode: true, fillOpacity: true },
},
];
const oldSpec = oldSpecWith({
chartAppearance: { fillMode: DashboardtypesFillModeDTO.none },
});
expect(buildPluginSpec(sections, { oldSpec }).chartAppearance).toStrictEqual(
{
fillMode: DashboardtypesFillModeDTO.solid,
},
);
});
it('carries a filled source fillMode unchanged', () => {
const sections: SectionConfig[] = [
{
kind: SectionKind.ChartAppearance,
controls: { fillMode: true, fillOpacity: true },
},
];
const oldSpec = oldSpecWith({
chartAppearance: { fillMode: DashboardtypesFillModeDTO.gradient },
});
expect(buildPluginSpec(sections, { oldSpec }).chartAppearance).toStrictEqual(
{
fillMode: DashboardtypesFillModeDTO.gradient,
},
);
});
// TimeSeries keeps all three modes, so an Area -> TimeSeries switch needs no coercion.
it('leaves fillMode alone for a kind that can be unfilled', () => {
const sections: SectionConfig[] = [
{ kind: SectionKind.ChartAppearance, controls: { fillMode: true } },
];
const oldSpec = oldSpecWith({
chartAppearance: { fillMode: DashboardtypesFillModeDTO.none },
});
expect(buildPluginSpec(sections, { oldSpec }).chartAppearance).toStrictEqual(
{
fillMode: DashboardtypesFillModeDTO.none,
},
);
});
it('carries fillOpacity only when the target declares it, including 0', () => {
const withOpacity: SectionConfig[] = [
{
kind: SectionKind.ChartAppearance,
controls: { fillMode: true, fillOpacity: true },
},
];
const withoutOpacity: SectionConfig[] = [
{ kind: SectionKind.ChartAppearance, controls: { fillMode: true } },
];
const oldSpec = oldSpecWith({
chartAppearance: {
fillMode: DashboardtypesFillModeDTO.gradient,
fillOpacity: 0,
},
});
expect(
buildPluginSpec(withOpacity, { oldSpec }).chartAppearance,
).toStrictEqual({
fillMode: DashboardtypesFillModeDTO.gradient,
fillOpacity: 0,
});
expect(
buildPluginSpec(withoutOpacity, { oldSpec }).chartAppearance,
).toStrictEqual({ fillMode: DashboardtypesFillModeDTO.gradient });
});
it('carries old values over the defaults, gated by the declared controls', () => {
const sections: SectionConfig[] = [
{
@@ -729,21 +551,6 @@ describe('buildPluginSpec', () => {
});
});
it('seeds the full Area default set, filled solid and stacked', () => {
expect(buildPluginSpec(areaSections)).toStrictEqual({
visualization: {
timePreference: DashboardtypesTimePreferenceDTO.global_time,
stack: DashboardtypesStackModeDTO.normal,
},
legend: { position: DashboardtypesLegendPositionDTO.bottom },
chartAppearance: {
lineStyle: DashboardtypesLineStyleDTO.solid,
lineInterpolation: DashboardtypesLineInterpolationDTO.spline,
fillMode: DashboardtypesFillModeDTO.solid,
},
});
});
it('returns an empty spec for List (only switchPanelKind, nothing to seed)', () => {
expect(buildPluginSpec(listSections)).toStrictEqual({});
});

View File

@@ -5,7 +5,6 @@ import {
DashboardtypesLineInterpolationDTO,
DashboardtypesLineStyleDTO,
type DashboardtypesPanelSpecDTO,
DashboardtypesStackModeDTO,
DashboardtypesThresholdFormatDTO,
DashboardtypesTextAlignDTO,
DashboardtypesTimePreferenceDTO,
@@ -120,43 +119,6 @@ function isEmptySlice(value: object): boolean {
: Object.keys(value).length === 0;
}
/**
* Translates stacking across a Bar↔Area switch rather than dropping it. Area's
* `percent` has no bar equivalent, so it collapses to stacked-on; a stack-mode kind
* with nothing to carry starts on `normal`.
*/
function translateStackingForKind(
controls: SectionControls[SectionKind.Visualization],
old: SectionSpecMap[SectionKind.Visualization] | undefined,
): Pick<
SectionSpecMap[SectionKind.Visualization],
'stack' | 'stackedBarChart'
> {
if (controls.stacking) {
if (old?.stackedBarChart !== undefined) {
return { stackedBarChart: old.stackedBarChart };
}
if (old?.stack !== undefined) {
return { stackedBarChart: old.stack !== DashboardtypesStackModeDTO.none };
}
return {};
}
if (controls.stackMode) {
if (old?.stack !== undefined) {
return { stack: old.stack };
}
if (old?.stackedBarChart !== undefined) {
return {
stack: old.stackedBarChart
? DashboardtypesStackModeDTO.normal
: DashboardtypesStackModeDTO.none,
};
}
return { stack: DashboardtypesStackModeDTO.normal };
}
return {};
}
const SECTION_SEEDS: SectionSeeds = {
[SectionKind.TextLayout]: {
specKey: 'presentation',
@@ -196,7 +158,10 @@ const SECTION_SEEDS: SectionSeeds = {
timePreference:
old?.timePreference ?? DashboardtypesTimePreferenceDTO.global_time,
}),
...translateStackingForKind(controls, old),
...(controls.stacking &&
old?.stackedBarChart !== undefined && {
stackedBarChart: old.stackedBarChart,
}),
...(controls.fillSpans &&
old?.fillSpans !== undefined && { fillSpans: old.fillSpans }),
};
@@ -239,8 +204,7 @@ const SECTION_SEEDS: SectionSeeds = {
const {
lineStyle = DashboardtypesLineStyleDTO.solid,
lineInterpolation = DashboardtypesLineInterpolationDTO.spline,
fillMode,
fillOpacity,
fillMode = DashboardtypesFillModeDTO.none,
showPoints,
spanGaps,
} = oldPluginSpec?.chartAppearance ?? {};
@@ -252,16 +216,7 @@ const SECTION_SEEDS: SectionSeeds = {
appearance.lineInterpolation = lineInterpolation;
}
if (controls.fillMode) {
const carried = fillMode ?? DashboardtypesFillModeDTO.none;
// An always-filled kind's wire enum has no `none`, so the save API would
// reject it. Keyed off the capability, not the kind.
appearance.fillMode =
controls.fillOpacity && carried === DashboardtypesFillModeDTO.none
? DashboardtypesFillModeDTO.solid
: carried;
}
if (controls.fillOpacity && typeof fillOpacity === 'number') {
appearance.fillOpacity = fillOpacity;
appearance.fillMode = fillMode;
}
if (controls.showPoints && showPoints !== undefined) {
appearance.showPoints = showPoints;

View File

@@ -1,14 +1,4 @@
import {
DashboardtypesAreaFillModeDTO,
DashboardtypesStackModeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { FillMode, StackMode } from 'lib/uPlotV2/config/types';
import {
resolveAreaFillMode,
resolveSpanGaps,
resolveStackMode,
} from '../resolvers';
import { resolveSpanGaps } from '../resolvers';
describe('resolveSpanGaps', () => {
it('parses a duration string into seconds when thresholding', () => {
@@ -43,43 +33,3 @@ describe('resolveSpanGaps', () => {
expect(resolveSpanGaps({ fillLessThan: '5m' })).toBe(300);
});
});
describe('resolveAreaFillMode', () => {
it('maps each wire value to its chart fill mode', () => {
expect(resolveAreaFillMode(DashboardtypesAreaFillModeDTO.solid)).toBe(
FillMode.Solid,
);
expect(resolveAreaFillMode(DashboardtypesAreaFillModeDTO.gradient)).toBe(
FillMode.Gradient,
);
});
// Includes a stale `none`, which the area wire enum no longer carries.
it('falls back to solid for a missing or unknown value', () => {
expect(resolveAreaFillMode(undefined)).toBe(FillMode.Solid);
expect(resolveAreaFillMode('none' as DashboardtypesAreaFillModeDTO)).toBe(
FillMode.Solid,
);
});
});
describe('resolveStackMode', () => {
it('maps each wire value to its chart stack mode', () => {
expect(resolveStackMode(DashboardtypesStackModeDTO.none)).toBe(
StackMode.None,
);
expect(resolveStackMode(DashboardtypesStackModeDTO.normal)).toBe(
StackMode.Normal,
);
expect(resolveStackMode(DashboardtypesStackModeDTO.percent)).toBe(
StackMode.Percent,
);
});
it('falls back to none for a missing or unknown value', () => {
expect(resolveStackMode(undefined)).toBe(StackMode.None);
expect(resolveStackMode('stretch' as DashboardtypesStackModeDTO)).toBe(
StackMode.None,
);
});
});

View File

@@ -1,18 +1,14 @@
import {
DashboardtypesAreaFillModeDTO,
DashboardtypesFillModeDTO,
DashboardtypesLegendPositionDTO,
DashboardtypesLineInterpolationDTO,
DashboardtypesLineStyleDTO,
DashboardtypesStackModeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import omit from 'lodash-es/omit';
import {
FillMode,
LineInterpolation,
LineStyle,
StackMode,
} from 'lib/uPlotV2/config/types';
/**
@@ -42,18 +38,6 @@ export const FILL_MODE_MAP: Record<DashboardtypesFillModeDTO, FillMode> = {
[DashboardtypesFillModeDTO.none]: FillMode.None,
};
/** An area panel is always filled, so it drops `none`. */
export const AREA_FILL_MODE_MAP: Record<
DashboardtypesAreaFillModeDTO,
FillMode
> = omit(FILL_MODE_MAP, DashboardtypesFillModeDTO.none);
export const STACK_MODE_MAP: Record<DashboardtypesStackModeDTO, StackMode> = {
[DashboardtypesStackModeDTO.none]: StackMode.None,
[DashboardtypesStackModeDTO.normal]: StackMode.Normal,
[DashboardtypesStackModeDTO.percent]: StackMode.Percent,
};
export const LEGEND_POSITION_MAP: Record<
DashboardtypesLegendPositionDTO,
LegendPosition

View File

@@ -1,20 +1,13 @@
import { rangeUtil } from '@grafana/data';
import {
type DashboardtypesAreaFillModeDTO,
DashboardtypesLegendPositionDTO,
DashboardtypesPrecisionOptionDTO,
type DashboardtypesSpanGapsDTO,
type DashboardtypesStackModeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { FillMode, StackMode } from 'lib/uPlotV2/config/types';
import {
AREA_FILL_MODE_MAP,
LEGEND_POSITION_MAP,
STACK_MODE_MAP,
} from './enumMaps';
import { LEGEND_POSITION_MAP } from './enumMaps';
// Resolvers turning raw `spec` chart-appearance fields into runtime chart
// values, falling back to chart defaults for missing/unknown input.
@@ -72,23 +65,3 @@ export function resolveLegendPosition(
}
return LegendPosition.BOTTOM;
}
/** Missing/unknown falls back to `Solid`; an area panel is never a bare line. */
export function resolveAreaFillMode(
fillMode: DashboardtypesAreaFillModeDTO | undefined,
): FillMode {
if (fillMode && fillMode in AREA_FILL_MODE_MAP) {
return AREA_FILL_MODE_MAP[fillMode];
}
return FillMode.Solid;
}
/** Missing/unknown falls back to `None` — series drawn independently. */
export function resolveStackMode(
stack: DashboardtypesStackModeDTO | undefined,
): StackMode {
if (stack && stack in STACK_MODE_MAP) {
return STACK_MODE_MAP[stack];
}
return StackMode.None;
}

View File

@@ -100,7 +100,6 @@ export function panelTypeToRequestType(
switch (panelType) {
case PANEL_TYPES.TIME_SERIES:
case PANEL_TYPES.BAR:
case PANEL_TYPES.AREA:
case PANEL_TYPES.HISTOGRAM:
return Querybuildertypesv5RequestTypeDTO.time_series;
case PANEL_TYPES.TABLE:

View File

@@ -12,7 +12,6 @@ export const panelTypeToExplorerView: Record<PANEL_TYPES, ExplorerViews> = {
[PANEL_TYPES.TABLE]: ExplorerViews.TABLE,
[PANEL_TYPES.VALUE]: ExplorerViews.TIMESERIES,
[PANEL_TYPES.BAR]: ExplorerViews.TIMESERIES,
[PANEL_TYPES.AREA]: ExplorerViews.TIMESERIES,
[PANEL_TYPES.PIE]: ExplorerViews.TIMESERIES,
[PANEL_TYPES.HISTOGRAM]: ExplorerViews.TIMESERIES,
// Dashboard-only visualisation; explorers never offer it.

View File

@@ -7,12 +7,10 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
"github.com/SigNoz/signoz/pkg/types/opamptypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -26,11 +24,10 @@ const unmappedModelsLookback = time.Hour
type module struct {
store llmpricingruletypes.Store
querier querier.Querier
flagger flagger.Flagger
}
func NewModule(store llmpricingruletypes.Store, flagger flagger.Flagger, querier querier.Querier) llmpricingrule.Module {
return &module{store: store, flagger: flagger, querier: querier}
func NewModule(store llmpricingruletypes.Store, querier querier.Querier) llmpricingrule.Module {
return &module{store: store, querier: querier}
}
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
@@ -123,16 +120,6 @@ func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []byte, configVersion *opamptypes.AgentConfigVersion) ([]byte, string, error) {
ctx := context.Background()
// Skip the llm pricing processor unless AI observability is enabled for the org.
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
enabled, err := module.flagger.Boolean(ctx, flagger.FeatureEnableAIObservability, evalCtx)
if err != nil {
return nil, "", err
}
if !enabled {
return currentConfYaml, "", nil
}
rules, err := module.getEnabledRules(ctx, orgID)
if err != nil {
return nil, "", err

View File

@@ -5,22 +5,19 @@ import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/opamptypes"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
store spantypes.SpanMapperStore
flagger flagger.Flagger
store spantypes.SpanMapperStore
}
func NewModule(store spantypes.SpanMapperStore, flagger flagger.Flagger) spanmapper.Module {
return &module{store: store, flagger: flagger}
func NewModule(store spantypes.SpanMapperStore) spanmapper.Module {
return &module{store: store}
}
func (module *module) ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error) {
@@ -168,16 +165,6 @@ func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []byte, configVersion *opamptypes.AgentConfigVersion) ([]byte, string, error) {
ctx := context.Background()
// Skip the llm pricing processor unless AI observability is enabled for the org.
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
enabled, err := module.flagger.Boolean(ctx, flagger.FeatureEnableAIObservability, evalCtx)
if err != nil {
return nil, "", err
}
if !enabled {
return currentConfYaml, "", nil
}
enabledMappers, err := module.listEnabledGroupsWithMappers(ctx, orgID)
if err != nil {
return nil, "", err
@@ -188,7 +175,7 @@ func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []
return nil, "", err
}
serialized, err := json.Marshal(enabled)
serialized, err := json.Marshal(enabledMappers)
if err != nil {
return nil, "", err
}

View File

@@ -162,8 +162,8 @@ func NewModules(
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger), ruleStore),
CloudIntegration: cloudIntegrationModule,
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore), providerSettings, config.TraceDetail),
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore), fl),
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), fl, querier),
SpanMapper: implspanmapper.NewModule(implspanmapper.NewStore(sqlstore)),
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier),
Tag: tagModule,
}
}

View File

@@ -1467,264 +1467,6 @@ func TestTimeSeriesPanelDefaults(t *testing.T) {
}
}
func TestAreaChartPanelDefaults(t *testing.T) {
data := []byte(`{
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"links": [],
"plugin": {
"kind": "signoz/AreaChartPanel",
"spec": {}
},
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
}
},
"links": [],
"layouts": []
}`)
d, err := unmarshalDashboard(data)
require.NoError(t, err, "unmarshal and validate failed")
require.IsType(t, &AreaChartPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
spec := d.Panels["p1"].Spec.Plugin.Spec.(*AreaChartPanelSpec)
assert.Equal(t, "solid", spec.ChartAppearance.FillMode.ValueOrDefault(), "area fillMode defaults to solid, where the TimeSeries FillMode defaults to none")
assert.Nil(t, spec.ChartAppearance.FillOpacity, "an omitted fillOpacity stays nil so the renderer applies the kind default")
assert.Equal(t, "none", spec.Visualization.Stack.ValueOrDefault(), "expected Stack default none")
assert.Equal(t, "2", spec.Formatting.DecimalPrecision.ValueOrDefault(), "expected DecimalPrecision default 2")
assert.Equal(t, "spline", spec.ChartAppearance.LineInterpolation.ValueOrDefault(), "expected LineInterpolation default spline")
assert.Equal(t, "solid", spec.ChartAppearance.LineStyle.ValueOrDefault(), "expected LineStyle default solid")
assert.Equal(t, "global_time", spec.Visualization.TimePreference.ValueOrDefault(), "expected TimePreference default global_time")
assert.Equal(t, "bottom", spec.Legend.Position.ValueOrDefault(), "expected LegendPosition default bottom")
assert.Equal(t, "list", spec.Legend.Mode.ValueOrDefault(), "expected LegendMode default list")
output, err := json.Marshal(d)
require.NoError(t, err, "marshal dashboard failed")
outputStr := string(output)
for field, want := range map[string]string{
"fillMode": `"solid"`,
"stack": `"none"`,
"fillOpacity": `null`,
} {
assert.Contains(t, outputStr, `"`+field+`":`+want, "expected stored/response JSON to contain %s:%s", field, want)
}
}
func TestAreaChartPanelRoundTrip(t *testing.T) {
data := []byte(`{
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"links": [],
"plugin": {
"kind": "signoz/AreaChartPanel",
"spec": {
"visualization": {"timePreference": "global_time", "fillSpans": false, "stack": "percent"},
"chartAppearance": {"fillMode": "gradient", "fillOpacity": 0.4}
}
},
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
}
},
"links": [],
"layouts": []
}`)
d, err := unmarshalDashboard(data)
require.NoError(t, err, "unmarshal and validate failed")
require.IsType(t, &AreaChartPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
spec := d.Panels["p1"].Spec.Plugin.Spec.(*AreaChartPanelSpec)
assert.Equal(t, "percent", spec.Visualization.Stack.ValueOrDefault(), "expected stack percent")
assert.Equal(t, "gradient", spec.ChartAppearance.FillMode.ValueOrDefault(), "expected fillMode gradient")
output, err := json.Marshal(d)
require.NoError(t, err, "marshal dashboard failed")
assert.Contains(t, string(output), `"stack":"percent"`, "expected stack in stored/response JSON")
assert.Contains(t, string(output), `"fillMode":"gradient"`, "expected fillMode in stored/response JSON")
}
func TestAreaChartPanelFillOpacity(t *testing.T) {
tests := []struct {
scenario string
chartAppearance string
expectedFillOpacitySet bool
expectedFillOpacityValue FillOpacity
expectedMarshalledJSON string
}{
{
scenario: "zero is a set value, not an absent one",
chartAppearance: `{"fillOpacity": 0}`,
expectedFillOpacitySet: true,
expectedFillOpacityValue: 0,
expectedMarshalledJSON: `"fillOpacity":0`,
},
{
scenario: "fully opaque upper bound",
chartAppearance: `{"fillOpacity": 1}`,
expectedFillOpacitySet: true,
expectedFillOpacityValue: 1,
expectedMarshalledJSON: `"fillOpacity":1`,
},
{
scenario: "typical fractional value",
chartAppearance: `{"fillOpacity": 0.4}`,
expectedFillOpacitySet: true,
expectedFillOpacityValue: 0.4,
expectedMarshalledJSON: `"fillOpacity":0.4`,
},
{
scenario: "precision beyond one decimal place survives",
chartAppearance: `{"fillOpacity": 0.125}`,
expectedFillOpacitySet: true,
expectedFillOpacityValue: 0.125,
expectedMarshalledJSON: `"fillOpacity":0.125`,
},
{
scenario: "omitted field stays nil so the renderer applies the kind default",
chartAppearance: `{}`,
expectedFillOpacitySet: false,
expectedMarshalledJSON: `"fillOpacity":null`,
},
{
scenario: "explicit null stays nil rather than decoding as zero",
chartAppearance: `{"fillOpacity": null}`,
expectedFillOpacitySet: false,
expectedMarshalledJSON: `"fillOpacity":null`,
},
}
for _, test := range tests {
t.Run(test.scenario, func(t *testing.T) {
data := []byte(`{
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"links": [],
"plugin": {"kind": "signoz/AreaChartPanel", "spec": {"chartAppearance": ` + test.chartAppearance + `}},
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
}
},
"links": [],
"layouts": []
}`)
d, err := unmarshalDashboard(data)
require.NoError(t, err, "unmarshal and validate failed")
require.IsType(t, &AreaChartPanelSpec{}, d.Panels["p1"].Spec.Plugin.Spec)
spec := d.Panels["p1"].Spec.Plugin.Spec.(*AreaChartPanelSpec)
if !test.expectedFillOpacitySet {
assert.Nil(t, spec.ChartAppearance.FillOpacity, "expected fillOpacity to stay unset")
} else {
require.NotNil(t, spec.ChartAppearance.FillOpacity, "expected fillOpacity to decode as a set value")
assert.Equal(t, test.expectedFillOpacityValue, *spec.ChartAppearance.FillOpacity, "unexpected decoded fillOpacity")
}
output, err := json.Marshal(d)
require.NoError(t, err, "marshal dashboard failed")
assert.Contains(t, string(output), test.expectedMarshalledJSON, "unexpected fillOpacity in stored/response JSON")
})
}
}
func TestInvalidateAreaChartPanelSpecValues(t *testing.T) {
tests := []struct {
scenario string
panelKind string
panelSpec string
expectedErrorSubstring string
}{
{
scenario: "unknown stack mode",
panelKind: "signoz/AreaChartPanel",
panelSpec: `{"visualization": {"stack": "stacked"}}`,
expectedErrorSubstring: "stack mode",
},
{
scenario: "unknown area fill mode",
panelKind: "signoz/AreaChartPanel",
panelSpec: `{"chartAppearance": {"fillMode": "striped"}}`,
expectedErrorSubstring: "fill mode",
},
{
scenario: "fill opacity on a 0-100 scale",
panelKind: "signoz/AreaChartPanel",
panelSpec: `{"chartAppearance": {"fillOpacity": 40}}`,
expectedErrorSubstring: "invalid fillOpacity 40: must be between 0 and 1",
},
{
scenario: "negative fill opacity",
panelKind: "signoz/AreaChartPanel",
panelSpec: `{"chartAppearance": {"fillOpacity": -0.5}}`,
expectedErrorSubstring: "invalid fillOpacity -0.5: must be between 0 and 1",
},
{
scenario: "non-numeric fill opacity",
panelKind: "signoz/AreaChartPanel",
panelSpec: `{"chartAppearance": {"fillOpacity": "0.4"}}`,
expectedErrorSubstring: "cannot unmarshal string",
},
{
scenario: "stack on a time series panel",
panelKind: "signoz/TimeSeriesPanel",
panelSpec: `{"visualization": {"stack": "normal"}}`,
expectedErrorSubstring: `unknown field`,
},
{
scenario: "fill opacity on a time series panel",
panelKind: "signoz/TimeSeriesPanel",
panelSpec: `{"chartAppearance": {"fillOpacity": 0.4}}`,
expectedErrorSubstring: `unknown field`,
},
{
scenario: "stacked bar chart on an area panel",
panelKind: "signoz/AreaChartPanel",
panelSpec: `{"visualization": {"stackedBarChart": true}}`,
expectedErrorSubstring: `unknown field`,
},
{
scenario: "stack on a bar chart panel",
panelKind: "signoz/BarChartPanel",
panelSpec: `{"visualization": {"stack": "percent"}}`,
expectedErrorSubstring: `unknown field`,
},
}
for _, test := range tests {
t.Run(test.scenario, func(t *testing.T) {
data := []byte(`{
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"links": [],
"plugin": {"kind": "` + test.panelKind + `", "spec": ` + test.panelSpec + `},
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/PromQLQuery", "spec": {"name": "A", "query": "up"}}}}]
}
}
},
"links": [],
"layouts": []
}`)
_, err := unmarshalDashboard(data)
require.Error(t, err, "expected the spec to be rejected")
assert.Contains(t, err.Error(), test.expectedErrorSubstring, "unexpected error message: %s", err.Error())
})
}
}
func TestNumberPanelDefaults(t *testing.T) {
data := []byte(`{
"variables": [],

View File

@@ -30,7 +30,6 @@ func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return markDiscriminator(s, "kind", map[string]string{
string(PanelKindTimeSeries): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec"),
string(PanelKindBarChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec"),
string(PanelKindAreaChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec"),
string(PanelKindNumber): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec"),
string(PanelKindPieChart): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec"),
string(PanelKindTable): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec"),
@@ -62,7 +61,6 @@ func (PanelPlugin) JSONSchemaOneOf() []any {
return []any{
PanelPluginVariant[TimeSeriesPanelSpec]{Kind: string(PanelKindTimeSeries)},
PanelPluginVariant[BarChartPanelSpec]{Kind: string(PanelKindBarChart)},
PanelPluginVariant[AreaChartPanelSpec]{Kind: string(PanelKindAreaChart)},
PanelPluginVariant[NumberPanelSpec]{Kind: string(PanelKindNumber)},
PanelPluginVariant[PieChartPanelSpec]{Kind: string(PanelKindPieChart)},
PanelPluginVariant[TablePanelSpec]{Kind: string(PanelKindTable)},
@@ -227,7 +225,6 @@ var (
panelPluginSpecs = map[PanelPluginKind]func() any{
PanelKindTimeSeries: func() any { return new(TimeSeriesPanelSpec) },
PanelKindBarChart: func() any { return new(BarChartPanelSpec) },
PanelKindAreaChart: func() any { return new(AreaChartPanelSpec) },
PanelKindNumber: func() any { return new(NumberPanelSpec) },
PanelKindPieChart: func() any { return new(PieChartPanelSpec) },
PanelKindTable: func() any { return new(TablePanelSpec) },
@@ -251,7 +248,6 @@ var (
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindAreaChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindNumber: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindHistogram: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},

View File

@@ -188,8 +188,7 @@ func (d *DashboardV2) GetPanelQuery(startTime, endTime uint64, panelKey string)
return nil, err
}
// fillGaps lives on the panel visualization; only timeseries, bar chart and
// area chart carry it.
// fillGaps lives on the panel visualization; only timeseries and bar chart carry it.
fillGaps := false
switch panelSpec := panel.Spec.Plugin.Spec.(type) {
case *TimeSeriesPanelSpec:
@@ -200,10 +199,6 @@ func (d *DashboardV2) GetPanelQuery(startTime, endTime uint64, panelKey string)
if panelSpec != nil {
fillGaps = panelSpec.Visualization.FillSpans
}
case *AreaChartPanelSpec:
if panelSpec != nil {
fillGaps = panelSpec.Visualization.FillSpans
}
}
return &qb.QueryRangeRequest{

View File

@@ -168,7 +168,6 @@ type PanelPluginKind string
const (
PanelKindTimeSeries PanelPluginKind = "signoz/TimeSeriesPanel"
PanelKindBarChart PanelPluginKind = "signoz/BarChartPanel"
PanelKindAreaChart PanelPluginKind = "signoz/AreaChartPanel"
PanelKindNumber PanelPluginKind = "signoz/NumberPanel"
PanelKindPieChart PanelPluginKind = "signoz/PieChartPanel"
PanelKindTable PanelPluginKind = "signoz/TablePanel"
@@ -178,7 +177,7 @@ const (
)
func (PanelPluginKind) Enum() []any {
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindAreaChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText}
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText}
}
func (k PanelPluginKind) rendersWithoutQuery() bool {
@@ -210,30 +209,6 @@ type BarChartPanelSpec struct {
Thresholds []ThresholdWithLabel `json:"thresholds" validate:"dive"`
}
type AreaChartPanelSpec struct {
Visualization AreaChartVisualization `json:"visualization"`
Formatting PanelFormatting `json:"formatting"`
ChartAppearance AreaChartAppearance `json:"chartAppearance"`
Axes Axes `json:"axes"`
Legend Legend `json:"legend"`
Thresholds []ThresholdWithLabel `json:"thresholds" validate:"dive"`
}
// AreaChartAppearance repeats the line-drawing fields rather than embedding
// TimeSeriesChartAppearance: both carry a `fillMode` under different enums, and
// a duplicated json tag across an embed boundary is resolved by depth, which the
// schema reflector does not model.
type AreaChartAppearance struct {
LineInterpolation LineInterpolation `json:"lineInterpolation"`
ShowPoints bool `json:"showPoints"`
LineStyle LineStyle `json:"lineStyle"`
FillMode AreaFillMode `json:"fillMode"`
// FillOpacity is a pointer so an omitted field resolves to the kind default at
// render time; a plain value would make the Go zero value a transparent fill.
FillOpacity *FillOpacity `json:"fillOpacity"`
SpanGaps SpanGaps `json:"spanGaps"`
}
type NumberPanelSpec struct {
Visualization BasicVisualization `json:"visualization"`
Formatting PanelFormatting `json:"formatting"`
@@ -312,12 +287,6 @@ type BarChartVisualization struct {
StackedBarChart bool `json:"stackedBarChart"`
}
type AreaChartVisualization struct {
BasicVisualization
FillSpans bool `json:"fillSpans"`
Stack StackMode `json:"stack"`
}
type PanelFormatting struct {
Unit string `json:"unit"`
DecimalPrecision PrecisionOption `json:"decimalPrecision"`
@@ -678,106 +647,6 @@ func (fm *FillMode) UnmarshalJSON(data []byte) error {
}
}
type AreaFillMode struct{ valuer.String }
var (
AreaFillModeSolid = AreaFillMode{valuer.NewString("solid")} // default
AreaFillModeGradient = AreaFillMode{valuer.NewString("gradient")}
)
func (AreaFillMode) Enum() []any {
return []any{AreaFillModeSolid, AreaFillModeGradient}
}
func (fm AreaFillMode) ValueOrDefault() string {
if fm.IsZero() {
return AreaFillModeSolid.StringValue()
}
return fm.StringValue()
}
func (fm AreaFillMode) MarshalJSON() ([]byte, error) {
return json.Marshal(fm.ValueOrDefault())
}
func (fm *AreaFillMode) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid fill mode: must be a string, one of `solid`, `gradient`, or `none`")
}
val := AreaFillMode{valuer.NewString(v)}
switch val {
case AreaFillModeSolid, AreaFillModeGradient:
*fm = val
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid fill mode %q: must be `solid`, `gradient`, or `none`", v)
}
}
// StackMode is area-only. Bar stacking stays on BarChartVisualization.StackedBarChart,
// so `percent` is not reachable from a bar panel.
type StackMode struct{ valuer.String }
var (
StackModeNone = StackMode{valuer.NewString("none")} // default
StackModeNormal = StackMode{valuer.NewString("normal")}
StackModePercent = StackMode{valuer.NewString("percent")}
)
func (StackMode) Enum() []any {
return []any{StackModeNone, StackModeNormal, StackModePercent}
}
func (sm StackMode) ValueOrDefault() string {
if sm.IsZero() {
return StackModeNone.StringValue()
}
return sm.StringValue()
}
func (sm StackMode) MarshalJSON() ([]byte, error) {
return json.Marshal(sm.ValueOrDefault())
}
func (sm *StackMode) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid stack mode: must be a string, one of `none`, `normal`, or `percent`")
}
val := StackMode{valuer.NewString(v)}
switch val {
case StackModeNone, StackModeNormal, StackModePercent:
*sm = val
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid stack mode %q: must be `none`, `normal`, or `percent`", v)
}
}
// FillOpacity is the alpha of an area fill, in 01 because that is what the
// chart layer consumes directly. Unlike the enums in this section it has no
// ValueOrDefault: 0 is a legitimate value, so the kind default lives at render
// time behind a nil pointer.
type FillOpacity float64
func (FillOpacity) PrepareJSONSchema(s *jsonschema.Schema) error {
s.WithMinimum(0).WithMaximum(1)
return nil
}
func (o *FillOpacity) UnmarshalJSON(data []byte) error {
var v float64
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid fillOpacity: must be a number between 0 and 1")
}
if v < 0 || v > 1 {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid fillOpacity %v: must be between 0 and 1", v)
}
*o = FillOpacity(v)
return nil
}
type SpanGaps struct {
FillOnlyBelow bool `json:"fillOnlyBelow" description:"Controls whether lines connect across null values. When false (default), all gaps are connected. When true, only gaps smaller than fillLessThan are connected."`
FillLessThan string `json:"fillLessThan" description:"The maximum gap size to connect when fillOnlyBelow is true. Gaps larger than this duration are left disconnected."`

View File

@@ -14,11 +14,6 @@ import (
// (transition.dashboardMigrateV5). Pre-v5 builder queries will produce
// invalid v2 envelopes — run the v4→v5 migration first.
//
// The v1 input shape is closed: nothing writes v1 dashboards any more, so these
// files only ever convert what v1 could already express. Panel kinds and spec
// fields added to v2 from here on need no converter entry — change these files
// only when a v2 type edit breaks the build.
//
// The conversion is split across sibling files by concern:
// - perses_v1_to_v2_tags.go tags
// - perses_v1_to_v2_panels.go widgets → panels (+ panel field mappers)