Compare commits

...

3 Commits

Author SHA1 Message Date
Abhi Kumar
c170707757 refactor(dashboards): declare the legend colors control as its resolver
useLegendSeries switched on the panel kind to pick how a panel's output becomes
legend entries — pie slices or flat series — while the kinds that expose the
colors control were already declaring it as Legend.controls.colors. Two places
described the same fact, so a new chart kind could declare the control and
silently get an empty color picker.

The colors control now carries the resolver instead of a boolean: declaring the
resolver is declaring the control, so the two cannot disagree. The hook becomes a
lookup and a call with no switch, and LegendSection's truthiness check is
unchanged.

legendSeries.ts moves from PanelEditor/utils to Panels/utils — it only ever
imported from Panels and queryV5, and kinds cannot import up into the editor. Both
resolvers take one args object so pie needs no placeholder parameter.

Assisted-by: Claude Opus 5
2026-09-05 17:33:10 +05:30
Abhi Kumar
f6c34795a5 refactor(dashboards): read alert units and thresholds from the declarations
Both alert helpers switched on the panel kind: readPanelUnit listed the four
kinds that carry a unit, readPanelThresholds listed the shapes each kind's
thresholds take. Every new kind has to be added to both, and a missed one loses
its alert prefill silently — there is no failure, just a missing unit.

Both facts are already declared. A kind's Formatting section says whether it
exposes a unit, and its Thresholds section says which variant it edits, so
getSectionControls answers both by reading the kind's own sections.ts. It also
replaces the private copy of the same lookup in newPanelSeed.

A table variant contributes no prefill: its thresholds are per column, which has
no meaning for a panel-wide alert condition.

Assisted-by: Claude Opus 5
2026-09-05 17:32:59 +05:30
Abhi Kumar
37ef1cd1db refactor(dashboards): offer panel kinds from the registry
The new-panel picker and the editor's kind switcher both read a hand-maintained
PANEL_TYPES array. A kind absent from it renders fine on a saved dashboard but
can never be created or switched to, and nothing catches the omission.

Each kind now declares its picker icon next to the displayName it already
declares, and both surfaces render Object.values(PANELS). Registry declaration
order is display order, so registry.ts is reordered to keep today's tile order.
The parallel array and its PanelType interface are deleted.

Assisted-by: Claude Opus 5
2026-09-05 17:32:40 +05:30
35 changed files with 256 additions and 133 deletions

View File

@@ -8,7 +8,7 @@ import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panel
import { resolveSignal } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getBuilderQueries';
import type { EQueryType } from 'types/common/dashboard';
import type { LegendSeries } from '../utils/legendSeries';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import type { TableColumnOption } from '../hooks/useTableColumns';
import ConfigActions from './ConfigActions/ConfigActions';
import SectionSlot from './SectionSlot/SectionSlot';

View File

@@ -5,8 +5,18 @@ import PanelTypeSwitcher from '../PanelTypeSwitcher';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
// Stub the registry so the test doesn't pull in the real renderers and chart libs.
jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
getPanelDefinition: jest.fn(),
PANEL_OPTIONS: [
{ kind: 'signoz/TimeSeriesPanel', displayName: 'Time Series' },
{ kind: 'signoz/NumberPanel', displayName: 'Number' },
{ kind: 'signoz/TablePanel', displayName: 'Table' },
{ kind: 'signoz/BarChartPanel', displayName: 'Bar Chart' },
{ kind: 'signoz/PieChartPanel', displayName: 'Pie Chart' },
{ kind: 'signoz/HistogramPanel', displayName: 'Histogram' },
{ kind: 'signoz/ListPanel', displayName: 'List' },
].map((option) => ({ ...option, icon: (): null => null })),
}));
const mockGetPanelDefinition = getPanelDefinition as unknown as jest.Mock;

View File

@@ -2,8 +2,8 @@ import { useMemo } from 'react';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { EQueryType } from 'types/common/dashboard';
import { PANEL_OPTIONS } from '../../../Panels/registry';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { PANEL_TYPES } from '../../../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/constants';
import type { ConfigSelectItem } from '../controls/ConfigSelect/ConfigSelect';
import { getPanelTypeDisabledReason } from './utils';
@@ -27,17 +27,17 @@ export function usePanelTypeSelectItems({
}: UsePanelTypeSelectItemsArgs): ConfigSelectItem<PanelKind>[] {
return useMemo(
() =>
PANEL_TYPES.map(({ panelKind, label, Icon }) => {
PANEL_OPTIONS.map(({ kind, displayName, icon: Icon }) => {
// One reason drives both the disabled flag and the tooltip, so they can't disagree.
const disabledReason = getPanelTypeDisabledReason({
kind: panelKind,
kind,
queryType,
signal,
label,
label: displayName,
});
return {
value: panelKind,
label,
value: kind,
label: displayName,
icon: <Icon size={14} />,
disabled: !!disabledReason,
tooltip: disabledReason,

View File

@@ -5,7 +5,7 @@ import { Input } from 'antd';
import type { DashboardtypesLegendDTOCustomColors } from 'api/generated/services/sigNoz.schemas';
import { Virtuoso } from 'react-virtuoso';
import type { LegendSeries } from '../../../utils/legendSeries';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import LegendColorRow from './LegendColorRow';
import {
clearSeriesColor,

View File

@@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import type { LegendSeries } from '../../../../utils/legendSeries';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import LegendColors from '../LegendColors';
const SERIES: LegendSeries[] = [

View File

@@ -1,4 +1,4 @@
import type { LegendSeries } from '../../../../utils/legendSeries';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import {
clearSeriesColor,
filterLegendSeries,

View File

@@ -1,6 +1,6 @@
import type { DashboardtypesLegendDTOCustomColors } from 'api/generated/services/sigNoz.schemas';
import type { LegendSeries } from '../../../utils/legendSeries';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
/** Case-insensitive substring filter over series labels. Empty query → all series. */
export function filterLegendSeries(

View File

@@ -1,7 +1,7 @@
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { PanelKind } from '../../Panels/types/panelKind';
import type { LegendSeries } from '../utils/legendSeries';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import type { TableColumnOption } from '../hooks/useTableColumns';
import { EQueryType } from 'types/common/dashboard';

View File

@@ -11,6 +11,10 @@ jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
supportedSignals: ['metrics', 'logs', 'traces'],
supportedQueryTypes: ['builder', 'clickhouse_sql', 'promql'],
})),
PANEL_OPTIONS: [
{ kind: 'signoz/TimeSeriesPanel', displayName: 'Time Series' },
{ kind: 'signoz/TablePanel', displayName: 'Table' },
].map((option) => ({ ...option, icon: (): null => null })),
}));
// Open the antd Select by clicking its selector, then pick the option by label.

View File

@@ -1,36 +1,27 @@
import { useMemo } from 'react';
import { useIsDarkMode } from 'hooks/useDarkMode';
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
import { SectionKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import type { PanelQueryData } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
import {
type LegendSeries,
resolvePieLegendSeries,
resolveTimeSeriesLegendSeries,
} from '../utils/legendSeries';
/**
* Resolves the panel's rendered series into `{ label, defaultColor }` pairs so the
* legend-colors control can key overrides by the exact labels the chart draws. Only the
* kinds that expose a colors control resolve series (Pie from its scalar slices, Time
* Series from its flat series); every other kind returns none.
* legend-colors control can key overrides by the exact labels the chart draws, using
* the resolver the kind declares as its `colors` control.
*/
export function useLegendSeries(
panel: DashboardtypesPanelDTO,
data: PanelQueryData,
): LegendSeries[] {
const isDarkMode = useIsDarkMode();
const kind = panel.spec.plugin.kind;
return useMemo(() => {
switch (panel.spec.plugin.kind) {
case 'signoz/PieChartPanel':
return resolvePieLegendSeries(data, isDarkMode);
case 'signoz/TimeSeriesPanel':
case 'signoz/BarChartPanel':
case 'signoz/HistogramPanel':
return resolveTimeSeriesLegendSeries(panel.spec.queries, data, isDarkMode);
default:
return [];
}
}, [panel.spec.plugin.kind, panel.spec.queries, data, isDarkMode]);
const resolve = getSectionControls(kind, SectionKind.Legend)?.colors;
return resolve
? resolve({ queries: panel.spec.queries, data, isDarkMode })
: [];
}, [kind, panel.spec.queries, data, isDarkMode]);
}

View File

@@ -1,3 +1,5 @@
import { BarChart } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +12,7 @@ import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
kind: 'signoz/BarChartPanel',
displayName: 'Bar Chart',
icon: BarChart,
Renderer,
sections,
supportedSignals: [

View File

@@ -1,3 +1,4 @@
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
import {
SectionKind,
ThresholdVariant,
@@ -13,7 +14,10 @@ export const sections: SectionConfig[] = [
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
{
kind: SectionKind.Legend,
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
},
{
kind: SectionKind.Thresholds,
controls: { variant: ThresholdVariant.LABEL },

View File

@@ -1,3 +1,5 @@
import { BarChart } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +12,7 @@ import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
kind: 'signoz/HistogramPanel',
displayName: 'Histogram',
icon: BarChart,
Renderer,
sections,
supportedSignals: [

View File

@@ -1,3 +1,4 @@
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { SectionKind, type SectionConfig } from '../../types/sections';
@@ -9,7 +10,7 @@ export const sections: SectionConfig[] = [
},
{
kind: SectionKind.Legend,
controls: { position: true, colors: true },
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
// Merging all queries collapses to one distribution with no legend.
isHidden: (spec): boolean =>
Boolean(

View File

@@ -1,3 +1,5 @@
import { List } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -11,6 +13,7 @@ import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/ListPanel'> = {
kind: 'signoz/ListPanel',
displayName: 'List',
icon: List,
Renderer,
// Raw records come from logs and traces; metrics don't produce row data.
supportedSignals: [

View File

@@ -1,3 +1,5 @@
import { Hash } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +12,7 @@ import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/NumberPanel'> = {
kind: 'signoz/NumberPanel',
displayName: 'Number',
icon: Hash,
Renderer,
sections,
supportedSignals: [

View File

@@ -1,3 +1,5 @@
import { ChartPie } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +12,7 @@ import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
kind: 'signoz/PieChartPanel',
displayName: 'Pie Chart',
icon: ChartPie,
Renderer,
sections,
supportedSignals: [

View File

@@ -1,3 +1,4 @@
import { resolvePieLegendSeries } from '../../utils/legendSeries';
import { SectionKind, type SectionConfig } from '../../types/sections';
// Pie has no axes, thresholds, or stacking — just value formatting and a legend
@@ -8,6 +9,9 @@ export const sections: SectionConfig[] = [
controls: { switchPanelKind: true, timePreference: true },
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
{
kind: SectionKind.Legend,
controls: { position: true, colors: resolvePieLegendSeries },
},
{ kind: SectionKind.ContextLinks },
];

View File

@@ -1,3 +1,5 @@
import { Table } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +12,7 @@ import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/TablePanel'> = {
kind: 'signoz/TablePanel',
displayName: 'Table',
icon: Table,
Renderer,
sections,
supportedSignals: [

View File

@@ -1,3 +1,5 @@
import { ChartLine } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +12,7 @@ import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
kind: 'signoz/TimeSeriesPanel',
displayName: 'Time Series',
icon: ChartLine,
Renderer,
sections,
supportedSignals: [

View File

@@ -1,3 +1,4 @@
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
import {
SectionKind,
ThresholdVariant,
@@ -11,7 +12,10 @@ export const sections: SectionConfig[] = [
},
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
{
kind: SectionKind.Legend,
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
},
{
kind: SectionKind.ChartAppearance,
controls: {

View File

@@ -1,4 +1,5 @@
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { TriangleAlert } from '@signozhq/icons';
import {
NO_PANEL_ACTIONS,
@@ -18,6 +19,8 @@ import Renderer from './Renderer';
export const UNSUPPORTED_PANEL: RenderablePanelDefinition = {
kind: '<unsupported>' as RenderablePanelDefinition['kind'],
displayName: 'Unsupported panel',
// Never offered in the UI — the kind lists come from the registry, which omits this.
icon: TriangleAlert,
Renderer,
sections: [],
supportedSignals: [],

View File

@@ -7,22 +7,33 @@ import { definition as Table } from './kinds/TablePanel/definition';
import { definition as List } from './kinds/ListPanel/definition';
import { UNSUPPORTED_PANEL } from './kinds/UnsupportedPanel/definition';
import type {
PanelDefinition,
PanelRegistry,
RenderablePanelDefinition,
} from './types/panelDefinition';
import { PanelKind } from './types/panelKind';
// Each kind owns its PanelDefinition; registering a new panel is one entry here.
// Declaration order is the order kinds are offered in the UI.
export const PANELS: PanelRegistry = {
[TimeSeries.kind]: TimeSeries,
[BarChart.kind]: BarChart,
[Histogram.kind]: Histogram,
[NumberValue.kind]: NumberValue,
[PieChart.kind]: PieChart,
[Table.kind]: Table,
[BarChart.kind]: BarChart,
[PieChart.kind]: PieChart,
[Histogram.kind]: Histogram,
[List.kind]: List,
};
export type PanelOption = Pick<
PanelDefinition,
'kind' | 'displayName' | 'icon'
>;
// Backs both the new-panel picker and the editor's kind switcher; derived from PANELS
// so a registered kind can't end up unreachable from the UI.
export const PANEL_OPTIONS: PanelOption[] = Object.values(PANELS);
/**
* Whether this build can render the kind. `PanelKind` spans every kind the API declares,
* but a dashboard spec written by a newer SigNoz can name one this client has never heard

View File

@@ -1,5 +1,6 @@
import type { ComponentType } from 'react';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { ChartLine } from '@signozhq/icons';
import type { EQueryType } from 'types/common/dashboard';
import type { SectionConfig } from './sections';
@@ -60,9 +61,14 @@ export const NO_PANEL_ACTIONS: PanelActionCapabilities = {
drilldown: false,
};
// Derived from an icon component so the props stay exact (size is a constrained
// IconSize union) and ForwardRef-compatible.
export type PanelIcon = typeof ChartLine;
export interface PanelDefinition<K extends PanelKind = PanelKind> {
kind: K;
displayName: string;
icon: PanelIcon;
Renderer: ComponentType<PanelRendererProps<K>>;
sections: SectionConfig[];
/** Signals this kind can visualize. */

View File

@@ -13,6 +13,7 @@ import type {
DashboardtypesTimeSeriesChartAppearanceDTO,
TelemetrytypesTelemetryFieldKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { LegendSeriesResolver } from '../utils/legendSeries';
import {
Antenna,
BarChart,
@@ -105,7 +106,12 @@ export interface SectionControls {
columnUnits?: boolean;
};
[SectionKind.Axes]: { minMax?: boolean; logScale?: boolean }; // minMax → softMin/softMax
[SectionKind.Legend]: { position?: boolean; colors?: boolean }; // colors → customColors
[SectionKind.Legend]: {
position?: boolean;
// colors → customColors; the resolver supplies the labels overrides are keyed by,
// so a kind can't offer color overrides with nothing to color
colors?: LegendSeriesResolver;
};
[SectionKind.ChartAppearance]: {
lineStyle?: boolean;
lineInterpolation?: boolean;

View File

@@ -79,7 +79,7 @@ describe('buildPluginSpec', () => {
it('omits the key entirely when a seed produces an empty slice (never key: undefined)', () => {
const result = buildPluginSpec([
{ kind: SectionKind.Legend, controls: { colors: true } },
{ kind: SectionKind.Legend, controls: { colors: (): [] => [] } },
]);
expect(result).toStrictEqual({});
@@ -129,7 +129,7 @@ describe('buildPluginSpec', () => {
it('seeds neither when their defaulting controls are absent', () => {
const sections: SectionConfig[] = [
{ kind: SectionKind.Visualization, controls: { switchPanelKind: true } },
{ kind: SectionKind.Legend, controls: { colors: true } },
{ kind: SectionKind.Legend, controls: { colors: (): [] => [] } },
];
expect(buildPluginSpec(sections)).toStrictEqual({});
});
@@ -180,7 +180,10 @@ describe('buildPluginSpec', () => {
it('carries old legend position but never customColors', () => {
const sections: SectionConfig[] = [
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
{
kind: SectionKind.Legend,
controls: { position: true, colors: (): [] => [] },
},
];
const oldSpec = oldSpecWith({
legend: {

View File

@@ -0,0 +1,46 @@
import { SectionKind, ThresholdVariant } from '../../types/sections';
import { getSectionControls } from '../getSectionControls';
describe('getSectionControls', () => {
it('returns the controls a kind declares for a section', () => {
expect(
getSectionControls('signoz/TimeSeriesPanel', SectionKind.Formatting),
).toStrictEqual({ unit: true, decimals: true });
});
it('distinguishes kinds that key units per column from kinds with a panel unit', () => {
expect(
getSectionControls('signoz/TablePanel', SectionKind.Formatting)?.unit,
).toBeUndefined();
expect(
getSectionControls('signoz/TablePanel', SectionKind.Formatting)?.columnUnits,
).toBe(true);
});
it('reports the threshold variant each kind edits', () => {
expect(
getSectionControls('signoz/NumberPanel', SectionKind.Thresholds)?.variant,
).toBe(ThresholdVariant.COMPARISON);
expect(
getSectionControls('signoz/BarChartPanel', SectionKind.Thresholds)?.variant,
).toBe(ThresholdVariant.LABEL);
});
it('returns undefined when the kind does not expose the section', () => {
expect(
getSectionControls('signoz/ListPanel', SectionKind.Formatting),
).toBeUndefined();
expect(
getSectionControls('signoz/HistogramPanel', SectionKind.Thresholds),
).toBeUndefined();
});
it('returns undefined for an unregistered kind', () => {
expect(
getSectionControls(
'signoz/FuturePanel' as Parameters<typeof getSectionControls>[0],
SectionKind.Formatting,
),
).toBeUndefined();
});
});

View File

@@ -0,0 +1,22 @@
import { getPanelDefinition } from '../registry';
import type { PanelKind } from '../types/panelKind';
import type { ControlledSectionKind, SectionControls } from '../types/sections';
/**
* The controls a kind declares for one section, or `undefined` when it doesn't expose
* that section — so callers read `kinds/<Kind>/sections.ts` instead of switching on kind.
*/
export function getSectionControls<K extends ControlledSectionKind>(
kind: PanelKind,
sectionKind: K,
): SectionControls[K] | undefined {
const section = getPanelDefinition(kind).sections.find(
(candidate) => candidate.kind === sectionKind,
);
if (!section || !('controls' in section)) {
return undefined;
}
// `find` can't correlate the matched member's `controls` with `sectionKind`; the
// SectionConfig union guarantees it.
return section.controls as SectionControls[K];
}

View File

@@ -2,9 +2,9 @@ import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schem
import { themeColors } from 'constants/theme';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { preparePieData } from 'pages/DashboardPage/DashboardContainer/Panels/kinds/PieChartPanel/prepareData';
import { getBuilderQueries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getBuilderQueries';
import { resolveSeriesLabelV5 } from 'pages/DashboardPage/DashboardContainer/Panels/utils/resolveSeriesLabel';
import { preparePieData } from '../kinds/PieChartPanel/prepareData';
import { getBuilderQueries } from './getBuilderQueries';
import { resolveSeriesLabelV5 } from './resolveSeriesLabel';
import { prepareScalarTables } from 'pages/DashboardPage/DashboardContainer/queryV5/prepareScalarTables';
import type { PanelQueryData } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
import {
@@ -22,6 +22,15 @@ export interface LegendSeries {
type PanelQueries = DashboardtypesPanelDTO['spec']['queries'];
export interface LegendSeriesArgs {
queries: PanelQueries;
data: PanelQueryData;
isDarkMode: boolean;
}
/** Resolves a kind's output into the legend entries the colors control keys overrides by. */
export type LegendSeriesResolver = (args: LegendSeriesArgs) => LegendSeries[];
/**
* Dedupes `labels` (first-seen order, empties dropped) into `{ label, defaultColor }`
* pairs, resolving each unique label's color lazily via `colorFor` so a repeated
@@ -48,10 +57,10 @@ function buildLegendSeries(
* draws (without overrides, so their colors are the defaults) so the color control keys
* overrides by the same labels the chart does.
*/
export function resolvePieLegendSeries(
data: PanelQueryData,
isDarkMode: boolean,
): LegendSeries[] {
export function resolvePieLegendSeries({
data,
isDarkMode,
}: LegendSeriesArgs): LegendSeries[] {
const slices = preparePieData({
tables: prepareScalarTables({
results: getScalarResults(data.response),
@@ -70,11 +79,11 @@ export function resolvePieLegendSeries(
* Time-series kinds: resolve each flattened series' label the way the renderer does
* (`getLabelName` `resolveSeriesLabelV5`) and color it with `generateColor`.
*/
export function resolveTimeSeriesLegendSeries(
queries: PanelQueries,
data: PanelQueryData,
isDarkMode: boolean,
): LegendSeries[] {
export function resolveTimeSeriesLegendSeries({
queries,
data,
isDarkMode,
}: LegendSeriesArgs): LegendSeries[] {
const palette = isDarkMode
? themeColors.chartcolors
: themeColors.lightModeColor;

View File

@@ -4,8 +4,8 @@ import { DialogWrapper } from '@signozhq/ui/dialog';
import cx from 'classnames';
import { useDashboardSections } from '../../../hooks/useDashboardSections';
import { PANEL_OPTIONS } from '../../../Panels/registry';
import type { PanelKind } from '../../../Panels/types/panelKind';
import { PANEL_TYPES } from './constants';
import PanelTypeSelectionModalFooter from './PanelTypeSelectionModalFooter';
import { buildSectionOptions, resolveDefaultSectionValue } from './utils';
import styles from './PanelTypeSelectionModal.module.scss';
@@ -91,19 +91,19 @@ function PanelTypeSelectionModal({
<span className={styles.pickerLabel}>Select panel type</span>
)}
<div className={styles.grid}>
{PANEL_TYPES.map(({ panelKind, label, Icon }) => (
{PANEL_OPTIONS.map(({ kind, displayName, icon: Icon }) => (
<button
key={panelKind}
key={kind}
type="button"
className={cx(styles.panelTypeCard, {
[styles.panelTypeCardSelected]: panelKind === selectedPanelKind,
[styles.panelTypeCardSelected]: kind === selectedPanelKind,
})}
data-testid={`panel-type-${panelKind}`}
aria-pressed={panelKind === selectedPanelKind}
onClick={(): void => handleTileClick(panelKind)}
data-testid={`panel-type-${kind}`}
aria-pressed={kind === selectedPanelKind}
onClick={(): void => handleTileClick(kind)}
>
<Icon size={24} color={Color.BG_ROBIN_400} />
{label}
{displayName}
</button>
))}
</div>

View File

@@ -1,24 +0,0 @@
import {
BarChart,
ChartLine,
ChartPie,
Hash,
List,
Table,
} from '@signozhq/icons';
import type { PanelType } from './types';
export const PANEL_TYPES: PanelType[] = [
{
panelKind: 'signoz/TimeSeriesPanel',
label: 'Time Series',
Icon: ChartLine,
},
{ panelKind: 'signoz/NumberPanel', label: 'Number', Icon: Hash },
{ panelKind: 'signoz/TablePanel', label: 'Table', Icon: Table },
{ panelKind: 'signoz/BarChartPanel', label: 'Bar Chart', Icon: BarChart },
{ panelKind: 'signoz/PieChartPanel', label: 'Pie Chart', Icon: ChartPie },
{ panelKind: 'signoz/HistogramPanel', label: 'Histogram', Icon: BarChart },
{ panelKind: 'signoz/ListPanel', label: 'List', Icon: List },
];

View File

@@ -1,20 +1,11 @@
import type { IconSize } from '@signozhq/icons';
import type { ComponentType, SVGProps } from 'react';
import type { PanelKind } from '../../../Panels/types/panelKind';
type IconProps = Omit<SVGProps<SVGSVGElement>, 'ref'> & {
size?: number | IconSize;
strokeWidth?: number;
};
export interface PanelType {
panelKind: PanelKind;
label: string;
/** Icon component — the consumer renders it and controls size/color/etc. */
Icon: ComponentType<IconProps>;
}
export interface SectionOption {
/** The section's `layoutIndex`, stringified for the Select value. */
value: string;

View File

@@ -8,24 +8,24 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import {
SectionKind,
type PanelFormattingSlice,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
import { fromPerses } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { deriveAlertPrefill, PanelAlertPrefill } from './deriveAlertPrefill';
/** The panel's configured y-axis unit, for the kinds that carry one. */
/** The panel's configured y-axis unit, for the kinds that declare one. */
export function readPanelUnit(
plugin: DashboardtypesPanelPluginDTO,
): string | undefined {
switch (plugin.kind) {
case 'signoz/TimeSeriesPanel':
case 'signoz/BarChartPanel':
case 'signoz/NumberPanel':
case 'signoz/PieChartPanel':
return plugin.spec.formatting?.unit;
default:
return undefined;
if (!getSectionControls(plugin.kind, SectionKind.Formatting)?.unit) {
return undefined;
}
return (plugin.spec as { formatting?: PanelFormattingSlice }).formatting?.unit;
}
/**

View File

@@ -11,7 +11,15 @@ import {
AlertThresholdOperator,
Threshold,
} from 'container/CreateAlertV2/context/types';
import { THRESHOLD_COLOR_DANGER_ORDER } from 'pages/DashboardPage/DashboardContainer/Panels/types/threshold';
import {
SectionKind,
ThresholdVariant,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import {
THRESHOLD_COLOR_DANGER_ORDER,
type ComparisonThresholdShape,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/threshold';
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
import type { MetricAggregation } from 'types/api/v5/queryRange';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ReduceOperators } from 'types/common/queryBuilder';
@@ -64,27 +72,35 @@ export function uniformReduceTo(query: Query): ReduceOperators | undefined {
: undefined;
}
/**
* The panel's thresholds, normalized for alert prefill, read through the variant the
* kind declares. A `table` variant contributes nothing: per-column thresholds have no
* meaning for a panel-wide alert condition.
*/
function readPanelThresholds(
plugin: DashboardtypesPanelPluginDTO,
): NormalizedPanelThreshold[] {
switch (plugin.kind) {
case 'signoz/TimeSeriesPanel':
case 'signoz/BarChartPanel':
return (plugin.spec.thresholds ?? []).map((t) => ({
color: t.color,
value: t.value,
unit: t.unit,
}));
case 'signoz/NumberPanel':
return (plugin.spec.thresholds ?? []).map((t) => ({
color: t.color,
value: t.value,
unit: t.unit,
operator: t.operator,
}));
default:
return [];
const variant = getSectionControls(
plugin.kind,
SectionKind.Thresholds,
)?.variant;
if (
variant !== ThresholdVariant.LABEL &&
variant !== ThresholdVariant.COMPARISON
) {
return [];
}
const thresholds =
(plugin.spec as { thresholds?: ComparisonThresholdShape[] }).thresholds ?? [];
return thresholds.map((threshold) => ({
color: threshold.color,
value: threshold.value,
unit: threshold.unit,
// Only comparison thresholds carry an operator.
...(variant === ThresholdVariant.COMPARISON && {
operator: threshold.operator,
}),
}));
}
// Match case-insensitively (picker emits lowercase hex); unknown colors sort last.

View File

@@ -12,6 +12,7 @@ import {
buildPluginSpec,
type SeededPluginSpec,
} from '../DashboardContainer/Panels/utils/buildPluginSpec';
import { getSectionControls } from '../DashboardContainer/Panels/utils/getSectionControls';
import { toPerses } from '../DashboardContainer/queryV5/persesQueryAdapters';
interface NewPanelSeed {
@@ -21,15 +22,6 @@ interface NewPanelSeed {
pluginSpec: SeededPluginSpec;
}
function kindSupportsUnit(kind: PanelKind): boolean {
return getPanelDefinition(kind).sections.some(
(section) =>
section.kind === SectionKind.Formatting &&
'controls' in section &&
section.controls.unit === true,
);
}
/** Kind to fall back to for a query language a builder-only kind (List) can't hold. */
const FALLBACK_KIND_BY_QUERY_TYPE: Partial<Record<EQueryType, PanelKind>> = {
[EQueryType.PROM]: 'signoz/TimeSeriesPanel',
@@ -74,7 +66,10 @@ export function buildNewPanelSeed(
const queries = converted.length > 0 ? converted : buildDefaultQueries(kind);
// Explorers put the single `unit` on the query itself, not the panel spec.
if (compositeQuery.unit && kindSupportsUnit(kind)) {
if (
compositeQuery.unit &&
getSectionControls(kind, SectionKind.Formatting)?.unit
) {
return {
kind,
queries,