mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-06 11:30:40 +01:00
Compare commits
5 Commits
refactor/p
...
nv/text-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf57cf28c | ||
|
|
8f5f7fe23b | ||
|
|
edf7c74097 | ||
|
|
40c7794d0d | ||
|
|
ca618573cb |
@@ -3064,6 +3064,11 @@ components:
|
||||
- tags
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesHeaderOptions:
|
||||
properties:
|
||||
hide:
|
||||
type: boolean
|
||||
type: object
|
||||
DashboardtypesHistogramBuckets:
|
||||
properties:
|
||||
bucketCount:
|
||||
@@ -3421,6 +3426,7 @@ components:
|
||||
signoz/NumberPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
|
||||
signoz/PieChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
|
||||
signoz/TablePanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
|
||||
signoz/TextPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
|
||||
signoz/TimeSeriesPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
|
||||
propertyName: kind
|
||||
oneOf:
|
||||
@@ -3431,6 +3437,7 @@ components:
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
|
||||
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
|
||||
type: object
|
||||
DashboardtypesPanelPluginKind:
|
||||
enum:
|
||||
@@ -3441,6 +3448,7 @@ components:
|
||||
- signoz/TablePanel
|
||||
- signoz/HistogramPanel
|
||||
- signoz/ListPanel
|
||||
- signoz/TextPanel
|
||||
type: string
|
||||
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
|
||||
properties:
|
||||
@@ -3514,6 +3522,18 @@ components:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
enum:
|
||||
- signoz/TextPanel
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/DashboardtypesTextPanelSpec'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec:
|
||||
properties:
|
||||
kind:
|
||||
@@ -3805,6 +3825,37 @@ components:
|
||||
- color
|
||||
- columnName
|
||||
type: object
|
||||
DashboardtypesTextAlign:
|
||||
enum:
|
||||
- left
|
||||
- center
|
||||
- right
|
||||
type: string
|
||||
DashboardtypesTextMode:
|
||||
enum:
|
||||
- markdown
|
||||
type: string
|
||||
DashboardtypesTextPanelSpec:
|
||||
properties:
|
||||
headerOptions:
|
||||
$ref: '#/components/schemas/DashboardtypesHeaderOptions'
|
||||
mode:
|
||||
$ref: '#/components/schemas/DashboardtypesTextMode'
|
||||
presentation:
|
||||
$ref: '#/components/schemas/DashboardtypesTextPresentation'
|
||||
text:
|
||||
type: string
|
||||
type: object
|
||||
DashboardtypesTextPresentation:
|
||||
properties:
|
||||
background:
|
||||
nullable: true
|
||||
type: string
|
||||
textAlign:
|
||||
$ref: '#/components/schemas/DashboardtypesTextAlign'
|
||||
verticalAlign:
|
||||
$ref: '#/components/schemas/DashboardtypesVerticalAlign'
|
||||
type: object
|
||||
DashboardtypesTextVariableSpec:
|
||||
properties:
|
||||
constant:
|
||||
@@ -4014,6 +4065,12 @@ components:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
DashboardtypesVerticalAlign:
|
||||
enum:
|
||||
- top
|
||||
- center
|
||||
- bottom
|
||||
type: string
|
||||
ErrorsJSON:
|
||||
properties:
|
||||
code:
|
||||
|
||||
@@ -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 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../utils/legendSeries';
|
||||
import type { TableColumnOption } from '../hooks/useTableColumns';
|
||||
import ConfigActions from './ConfigActions/ConfigActions';
|
||||
import SectionSlot from './SectionSlot/SectionSlot';
|
||||
|
||||
@@ -5,18 +5,8 @@ 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;
|
||||
|
||||
@@ -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_OPTIONS.map(({ kind, displayName, icon: Icon }) => {
|
||||
PANEL_TYPES.map(({ panelKind, label, Icon }) => {
|
||||
// One reason drives both the disabled flag and the tooltip, so they can't disagree.
|
||||
const disabledReason = getPanelTypeDisabledReason({
|
||||
kind,
|
||||
kind: panelKind,
|
||||
queryType,
|
||||
signal,
|
||||
label: displayName,
|
||||
label,
|
||||
});
|
||||
return {
|
||||
value: kind,
|
||||
label: displayName,
|
||||
value: panelKind,
|
||||
label,
|
||||
icon: <Icon size={14} />,
|
||||
disabled: !!disabledReason,
|
||||
tooltip: disabledReason,
|
||||
|
||||
@@ -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 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../utils/legendSeries';
|
||||
import LegendColorRow from './LegendColorRow';
|
||||
import {
|
||||
clearSeriesColor,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../../utils/legendSeries';
|
||||
import LegendColors from '../LegendColors';
|
||||
|
||||
const SERIES: LegendSeries[] = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../../utils/legendSeries';
|
||||
import {
|
||||
clearSeriesColor,
|
||||
filterLegendSeries,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DashboardtypesLegendDTOCustomColors } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../../../utils/legendSeries';
|
||||
|
||||
/** Case-insensitive substring filter over series labels. Empty query → all series. */
|
||||
export function filterLegendSeries(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { PanelKind } from '../../Panels/types/panelKind';
|
||||
import type { LegendSeries } from 'pages/DashboardPage/DashboardContainer/Panels/utils/legendSeries';
|
||||
import type { LegendSeries } from '../utils/legendSeries';
|
||||
import type { TableColumnOption } from '../hooks/useTableColumns';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ 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.
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
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, using
|
||||
* the resolver the kind declares as its `colors` control.
|
||||
* 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.
|
||||
*/
|
||||
export function useLegendSeries(
|
||||
panel: DashboardtypesPanelDTO,
|
||||
data: PanelQueryData,
|
||||
): LegendSeries[] {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const kind = panel.spec.plugin.kind;
|
||||
|
||||
return useMemo(() => {
|
||||
const resolve = getSectionControls(kind, SectionKind.Legend)?.colors;
|
||||
return resolve
|
||||
? resolve({ queries: panel.spec.queries, data, isDarkMode })
|
||||
: [];
|
||||
}, [kind, panel.spec.queries, data, isDarkMode]);
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -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 '../kinds/PieChartPanel/prepareData';
|
||||
import { getBuilderQueries } from './getBuilderQueries';
|
||||
import { resolveSeriesLabelV5 } from './resolveSeriesLabel';
|
||||
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 { prepareScalarTables } from 'pages/DashboardPage/DashboardContainer/queryV5/prepareScalarTables';
|
||||
import type { PanelQueryData } from 'pages/DashboardPage/DashboardContainer/queryV5/types';
|
||||
import {
|
||||
@@ -22,15 +22,6 @@ 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
|
||||
@@ -57,10 +48,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,
|
||||
isDarkMode,
|
||||
}: LegendSeriesArgs): LegendSeries[] {
|
||||
export function resolvePieLegendSeries(
|
||||
data: PanelQueryData,
|
||||
isDarkMode: boolean,
|
||||
): LegendSeries[] {
|
||||
const slices = preparePieData({
|
||||
tables: prepareScalarTables({
|
||||
results: getScalarResults(data.response),
|
||||
@@ -79,11 +70,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,
|
||||
data,
|
||||
isDarkMode,
|
||||
}: LegendSeriesArgs): LegendSeries[] {
|
||||
export function resolveTimeSeriesLegendSeries(
|
||||
queries: PanelQueries,
|
||||
data: PanelQueryData,
|
||||
isDarkMode: boolean,
|
||||
): LegendSeries[] {
|
||||
const palette = isDarkMode
|
||||
? themeColors.chartcolors
|
||||
: themeColors.lightModeColor;
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BarChart } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
|
||||
kind: 'signoz/BarChartPanel',
|
||||
displayName: 'Bar Chart',
|
||||
icon: BarChart,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
@@ -14,10 +13,7 @@ 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: resolveTimeSeriesLegendSeries },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
{
|
||||
kind: SectionKind.Thresholds,
|
||||
controls: { variant: ThresholdVariant.LABEL },
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BarChart } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
|
||||
kind: 'signoz/HistogramPanel',
|
||||
displayName: 'Histogram',
|
||||
icon: BarChart,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
|
||||
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
@@ -10,7 +9,7 @@ export const sections: SectionConfig[] = [
|
||||
},
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: resolveTimeSeriesLegendSeries },
|
||||
controls: { position: true, colors: true },
|
||||
// Merging all queries collapses to one distribution with no legend.
|
||||
isHidden: (spec): boolean =>
|
||||
Boolean(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { List } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -13,7 +11,6 @@ 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: [
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Hash } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/NumberPanel'> = {
|
||||
kind: 'signoz/NumberPanel',
|
||||
displayName: 'Number',
|
||||
icon: Hash,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ChartPie } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
|
||||
kind: 'signoz/PieChartPanel',
|
||||
displayName: 'Pie Chart',
|
||||
icon: ChartPie,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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
|
||||
@@ -9,9 +8,6 @@ export const sections: SectionConfig[] = [
|
||||
controls: { switchPanelKind: true, timePreference: true },
|
||||
},
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: resolvePieLegendSeries },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Table } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/TablePanel'> = {
|
||||
kind: 'signoz/TablePanel',
|
||||
displayName: 'Table',
|
||||
icon: Table,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ChartLine } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,7 +10,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
|
||||
kind: 'signoz/TimeSeriesPanel',
|
||||
displayName: 'Time Series',
|
||||
icon: ChartLine,
|
||||
Renderer,
|
||||
sections,
|
||||
supportedSignals: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveTimeSeriesLegendSeries } from '../../utils/legendSeries';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
@@ -12,10 +11,7 @@ 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: resolveTimeSeriesLegendSeries },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
{
|
||||
kind: SectionKind.ChartAppearance,
|
||||
controls: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
|
||||
import {
|
||||
NO_PANEL_ACTIONS,
|
||||
@@ -19,8 +18,6 @@ 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: [],
|
||||
|
||||
@@ -7,33 +7,22 @@ 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,
|
||||
[NumberValue.kind]: NumberValue,
|
||||
[Table.kind]: Table,
|
||||
[BarChart.kind]: BarChart,
|
||||
[PieChart.kind]: PieChart,
|
||||
[Histogram.kind]: Histogram,
|
||||
[NumberValue.kind]: NumberValue,
|
||||
[PieChart.kind]: PieChart,
|
||||
[Table.kind]: Table,
|
||||
[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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -61,14 +60,9 @@ 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. */
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
DashboardtypesTimeSeriesChartAppearanceDTO,
|
||||
TelemetrytypesTelemetryFieldKeyDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { LegendSeriesResolver } from '../utils/legendSeries';
|
||||
import {
|
||||
Antenna,
|
||||
BarChart,
|
||||
@@ -106,12 +105,7 @@ export interface SectionControls {
|
||||
columnUnits?: boolean;
|
||||
};
|
||||
[SectionKind.Axes]: { minMax?: boolean; logScale?: boolean }; // minMax → softMin/softMax
|
||||
[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.Legend]: { position?: boolean; colors?: boolean }; // colors → customColors
|
||||
[SectionKind.ChartAppearance]: {
|
||||
lineStyle?: boolean;
|
||||
lineInterpolation?: boolean;
|
||||
|
||||
@@ -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: (): [] => [] } },
|
||||
{ kind: SectionKind.Legend, controls: { colors: true } },
|
||||
]);
|
||||
|
||||
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: (): [] => [] } },
|
||||
{ kind: SectionKind.Legend, controls: { colors: true } },
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
@@ -180,10 +180,7 @@ describe('buildPluginSpec', () => {
|
||||
|
||||
it('carries old legend position but never customColors', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Legend,
|
||||
controls: { position: true, colors: (): [] => [] },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true, colors: true } },
|
||||
];
|
||||
const oldSpec = oldSpecWith({
|
||||
legend: {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
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];
|
||||
}
|
||||
@@ -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_OPTIONS.map(({ kind, displayName, icon: Icon }) => (
|
||||
{PANEL_TYPES.map(({ panelKind, label, Icon }) => (
|
||||
<button
|
||||
key={kind}
|
||||
key={panelKind}
|
||||
type="button"
|
||||
className={cx(styles.panelTypeCard, {
|
||||
[styles.panelTypeCardSelected]: kind === selectedPanelKind,
|
||||
[styles.panelTypeCardSelected]: panelKind === selectedPanelKind,
|
||||
})}
|
||||
data-testid={`panel-type-${kind}`}
|
||||
aria-pressed={kind === selectedPanelKind}
|
||||
onClick={(): void => handleTileClick(kind)}
|
||||
data-testid={`panel-type-${panelKind}`}
|
||||
aria-pressed={panelKind === selectedPanelKind}
|
||||
onClick={(): void => handleTileClick(panelKind)}
|
||||
>
|
||||
<Icon size={24} color={Color.BG_ROBIN_400} />
|
||||
{displayName}
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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 },
|
||||
];
|
||||
@@ -1,11 +1,20 @@
|
||||
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;
|
||||
|
||||
@@ -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 declare one. */
|
||||
/** The panel's configured y-axis unit, for the kinds that carry one. */
|
||||
export function readPanelUnit(
|
||||
plugin: DashboardtypesPanelPluginDTO,
|
||||
): string | undefined {
|
||||
if (!getSectionControls(plugin.kind, SectionKind.Formatting)?.unit) {
|
||||
return undefined;
|
||||
switch (plugin.kind) {
|
||||
case 'signoz/TimeSeriesPanel':
|
||||
case 'signoz/BarChartPanel':
|
||||
case 'signoz/NumberPanel':
|
||||
case 'signoz/PieChartPanel':
|
||||
return plugin.spec.formatting?.unit;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
return (plugin.spec as { formatting?: PanelFormattingSlice }).formatting?.unit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,15 +11,7 @@ import {
|
||||
AlertThresholdOperator,
|
||||
Threshold,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
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 { THRESHOLD_COLOR_DANGER_ORDER } from 'pages/DashboardPage/DashboardContainer/Panels/types/threshold';
|
||||
import type { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { ReduceOperators } from 'types/common/queryBuilder';
|
||||
@@ -72,35 +64,27 @@ 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[] {
|
||||
const variant = getSectionControls(
|
||||
plugin.kind,
|
||||
SectionKind.Thresholds,
|
||||
)?.variant;
|
||||
if (
|
||||
variant !== ThresholdVariant.LABEL &&
|
||||
variant !== ThresholdVariant.COMPARISON
|
||||
) {
|
||||
return [];
|
||||
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 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.
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
buildPluginSpec,
|
||||
type SeededPluginSpec,
|
||||
} from '../DashboardContainer/Panels/utils/buildPluginSpec';
|
||||
import { getSectionControls } from '../DashboardContainer/Panels/utils/getSectionControls';
|
||||
import { toPerses } from '../DashboardContainer/queryV5/persesQueryAdapters';
|
||||
|
||||
interface NewPanelSeed {
|
||||
@@ -22,6 +21,15 @@ 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',
|
||||
@@ -66,10 +74,7 @@ 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 &&
|
||||
getSectionControls(kind, SectionKind.Formatting)?.unit
|
||||
) {
|
||||
if (compositeQuery.unit && kindSupportsUnit(kind)) {
|
||||
return {
|
||||
kind,
|
||||
queries,
|
||||
|
||||
@@ -114,8 +114,8 @@ func (d *DashboardSpec) validatePanels() error {
|
||||
return err
|
||||
}
|
||||
panelKind := panel.Spec.Plugin.Kind
|
||||
if len(panel.Spec.Queries) != 1 {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query, found %d", path, len(panel.Spec.Queries))
|
||||
if err := validatePanelQueryCount(panel.Spec.Queries, panelKind, path); err != nil {
|
||||
return err
|
||||
}
|
||||
allowed := allowedQueryKinds[panelKind]
|
||||
for qi, q := range panel.Spec.Queries {
|
||||
@@ -127,6 +127,22 @@ func (d *DashboardSpec) validatePanels() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePanelQueryCount(queries []Query, panelKind PanelPluginKind, path string) error {
|
||||
if queries == nil {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: is required and must not be null; use [] for a panel that renders without a query", path)
|
||||
}
|
||||
if panelKind.rendersWithoutQuery() {
|
||||
if len(queries) != 0 {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel kind %q renders without a query and must have queries: [], found %d", path, panelKind, len(queries))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(queries) != 1 {
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query, found %d", path, len(queries))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DashboardSpec) validateQuery(qi int, q Query, panelKind PanelPluginKind, path string, allowed []QueryPluginKind) error {
|
||||
queryPath := fmt.Sprintf("%s.spec.queries[%d].spec.plugin", path, qi)
|
||||
if err := validateQueryAllowedForPanel(q.Spec.Plugin, allowed, panelKind, queryPath); err != nil {
|
||||
|
||||
@@ -1086,7 +1086,7 @@ func TestInvalidatePanelWithoutQueries(t *testing.T) {
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
require.Error(t, err, "expected panel-without-queries to be rejected")
|
||||
assert.Contains(t, err.Error(), "panel must have one query")
|
||||
assert.Contains(t, err.Error(), "spec.queries: is required and must not be null")
|
||||
}
|
||||
|
||||
func TestInvalidatePanelWithEmptyQueriesArray(t *testing.T) {
|
||||
@@ -1136,6 +1136,155 @@ func TestInvalidatePanelWithMultipleDirectQueries(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "panel must have one query")
|
||||
}
|
||||
|
||||
func TestValidateTextPanel(t *testing.T) {
|
||||
wrapPanel := func(panelSpec string) []byte {
|
||||
return []byte(`{
|
||||
"variables": [],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TextPanel", "spec": ` + panelSpec + `},
|
||||
"queries": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
}
|
||||
|
||||
t.Run("fully specified text panel validates", func(t *testing.T) {
|
||||
d, err := unmarshalDashboard(wrapPanel(`{
|
||||
"mode": "markdown",
|
||||
"text": "# Runbook\n\nSee the [oncall doc](https://example.com).",
|
||||
"presentation": {"textAlign": "center", "verticalAlign": "bottom", "background": "#1A2b3C"},
|
||||
"headerOptions": {"hide": true}
|
||||
}`))
|
||||
require.NoError(t, err, "expected a fully specified text panel to validate")
|
||||
|
||||
spec, ok := d.Panels["p1"].Spec.Plugin.Spec.(*TextPanelSpec)
|
||||
require.True(t, ok, "expected the panel spec to decode as *TextPanelSpec")
|
||||
assert.Equal(t, TextModeMarkdown, spec.Mode)
|
||||
assert.Equal(t, "# Runbook\n\nSee the [oncall doc](https://example.com).", spec.Text)
|
||||
assert.Equal(t, TextAlignCenter, spec.Presentation.TextAlign)
|
||||
assert.Equal(t, VerticalAlignBottom, spec.Presentation.VerticalAlign)
|
||||
require.NotNil(t, spec.Presentation.Background, "expected background to be set")
|
||||
assert.Equal(t, "#1A2b3C", *spec.Presentation.Background)
|
||||
assert.True(t, spec.HeaderOptions.Hide)
|
||||
})
|
||||
|
||||
// The header shows unless explicitly hidden, so the zero value must round-trip
|
||||
// as a shown header. Background has no default: omitted stays omitted.
|
||||
t.Run("omitted fields marshal back as their defaults", func(t *testing.T) {
|
||||
d, err := unmarshalDashboard(wrapPanel(`{}`))
|
||||
require.NoError(t, err, "expected an empty text panel spec to validate")
|
||||
|
||||
spec, ok := d.Panels["p1"].Spec.Plugin.Spec.(*TextPanelSpec)
|
||||
require.True(t, ok, "expected the panel spec to decode as *TextPanelSpec")
|
||||
assert.Nil(t, spec.Presentation.Background, "expected an omitted background to stay unset")
|
||||
|
||||
out, err := json.Marshal(d.Panels["p1"].Spec.Plugin.Spec)
|
||||
require.NoError(t, err, "marshalling the decoded text panel spec")
|
||||
assert.JSONEq(t, `{
|
||||
"mode": "markdown",
|
||||
"text": "",
|
||||
"presentation": {"textAlign": "left", "verticalAlign": "top"},
|
||||
"headerOptions": {"hide": false}
|
||||
}`, string(out))
|
||||
})
|
||||
|
||||
t.Run("a text panel carrying a query is rejected", func(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"variables": [],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TextPanel", "spec": {"text": "hi"}},
|
||||
"queries": [{"kind": "time_series", "spec": {"plugin": {"kind": "signoz/BuilderQuery", "spec": {"name": "A", "signal": "metrics"}}}}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
require.Error(t, err, "expected a text panel with a query to be rejected")
|
||||
assert.Contains(t, err.Error(), "renders without a query and must have queries: [], found 1")
|
||||
})
|
||||
|
||||
t.Run("a text panel with null queries is rejected", func(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"variables": [],
|
||||
"panels": {
|
||||
"p1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"links": [],
|
||||
"plugin": {"kind": "signoz/TextPanel", "spec": {"text": "hi"}},
|
||||
"queries": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"layouts": []
|
||||
}`)
|
||||
_, err := unmarshalDashboard(data)
|
||||
require.Error(t, err, "expected a text panel with null queries to be rejected")
|
||||
assert.Contains(t, err.Error(), "spec.queries: is required and must not be null")
|
||||
})
|
||||
|
||||
t.Run("hex background colours validate", func(t *testing.T) {
|
||||
for _, background := range []string{"#abc", "#abcd", "#aabbcc", "#aabbccdd", "#AABBCC"} {
|
||||
d, err := unmarshalDashboard(wrapPanel(`{"presentation": {"background": "` + background + `"}}`))
|
||||
require.NoError(t, err, "expected background %q to validate", background)
|
||||
|
||||
spec, ok := d.Panels["p1"].Spec.Plugin.Spec.(*TextPanelSpec)
|
||||
require.True(t, ok, "expected the panel spec to decode as *TextPanelSpec")
|
||||
require.NotNil(t, spec.Presentation.Background)
|
||||
assert.Equal(t, background, *spec.Presentation.Background)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown enum values are rejected", func(t *testing.T) {
|
||||
for field, spec := range map[string]string{
|
||||
"mode": `{"mode": "html"}`,
|
||||
"textAlign": `{"presentation": {"textAlign": "justify"}}`,
|
||||
"verticalAlign": `{"presentation": {"verticalAlign": "middle"}}`,
|
||||
} {
|
||||
_, err := unmarshalDashboard(wrapPanel(spec))
|
||||
assert.Error(t, err, "expected an unknown %s value to be rejected", field)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid background colours are rejected", func(t *testing.T) {
|
||||
for name, spec := range map[string]string{
|
||||
"empty string": `{"presentation": {"background": ""}}`,
|
||||
"missing hash": `{"presentation": {"background": "aabbcc"}}`,
|
||||
"named colour": `{"presentation": {"background": "red"}}`,
|
||||
"wrong length": `{"presentation": {"background": "#abcde"}}`,
|
||||
"non hex digits": `{"presentation": {"background": "#gggggg"}}`,
|
||||
} {
|
||||
_, err := unmarshalDashboard(wrapPanel(spec))
|
||||
assert.Error(t, err, "expected %s background to be rejected", name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown spec fields are rejected", func(t *testing.T) {
|
||||
for field, spec := range map[string]string{
|
||||
"top level": `{"markdown": "hi"}`,
|
||||
"presentation": `{"presentation": {"horizontalAlign": "left"}}`,
|
||||
"headerOptions": `{"headerOptions": {"show": true}}`,
|
||||
} {
|
||||
_, err := unmarshalDashboard(wrapPanel(spec))
|
||||
assert.Error(t, err, "expected an unknown %s field to be rejected", field)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateRequiredFields(t *testing.T) {
|
||||
wrapVariable := func(pluginKind, pluginSpec string) string {
|
||||
return `{
|
||||
|
||||
@@ -35,6 +35,7 @@ func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
|
||||
string(PanelKindTable): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec"),
|
||||
string(PanelKindHistogram): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec"),
|
||||
string(PanelKindList): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec"),
|
||||
string(PanelKindText): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ func (PanelPlugin) JSONSchemaOneOf() []any {
|
||||
PanelPluginVariant[TablePanelSpec]{Kind: string(PanelKindTable)},
|
||||
PanelPluginVariant[HistogramPanelSpec]{Kind: string(PanelKindHistogram)},
|
||||
PanelPluginVariant[ListPanelSpec]{Kind: string(PanelKindList)},
|
||||
PanelPluginVariant[TextPanelSpec]{Kind: string(PanelKindText)},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +230,7 @@ var (
|
||||
PanelKindTable: func() any { return new(TablePanelSpec) },
|
||||
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
|
||||
PanelKindList: func() any { return new(ListPanelSpec) },
|
||||
PanelKindText: func() any { return new(TextPanelSpec) },
|
||||
}
|
||||
queryPluginSpecs = map[QueryPluginKind]func() any{
|
||||
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
|
||||
@@ -250,6 +253,7 @@ var (
|
||||
PanelKindPieChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
|
||||
PanelKindList: {QueryKindBuilder},
|
||||
PanelKindText: {},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -172,7 +172,12 @@ func (d *DashboardV2) GetPanelQuery(startTime, endTime uint64, panelKey string)
|
||||
if !ok || panel == nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidInput, "panel with key %q doesn't exist", panelKey)
|
||||
}
|
||||
// Validator guarantees exactly one query per panel.
|
||||
// A panel kind that renders from its own plugin spec has no query to execute;
|
||||
// asking for its query range is a client mistake.
|
||||
if panel.Spec.Plugin.Kind.rendersWithoutQuery() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "panel %q is a %q and has no query to execute", panelKey, panel.Spec.Plugin.Kind)
|
||||
}
|
||||
// Validator guarantees exactly one query for every other panel kind.
|
||||
if len(panel.Spec.Queries) != 1 {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "panel %q must have exactly one query", panelKey)
|
||||
}
|
||||
|
||||
@@ -173,10 +173,15 @@ const (
|
||||
PanelKindTable PanelPluginKind = "signoz/TablePanel"
|
||||
PanelKindHistogram PanelPluginKind = "signoz/HistogramPanel"
|
||||
PanelKindList PanelPluginKind = "signoz/ListPanel"
|
||||
PanelKindText PanelPluginKind = "signoz/TextPanel"
|
||||
)
|
||||
|
||||
func (PanelPluginKind) Enum() []any {
|
||||
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList}
|
||||
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText}
|
||||
}
|
||||
|
||||
func (k PanelPluginKind) rendersWithoutQuery() bool {
|
||||
return k == PanelKindText
|
||||
}
|
||||
|
||||
type TimeSeriesPanelSpec struct {
|
||||
@@ -237,6 +242,19 @@ type ListPanelSpec struct {
|
||||
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitzero" validate:"dive"`
|
||||
}
|
||||
|
||||
type TextPanelSpec struct {
|
||||
Mode TextMode `json:"mode"`
|
||||
Text string `json:"text"`
|
||||
Presentation TextPresentation `json:"presentation"`
|
||||
HeaderOptions HeaderOptions `json:"headerOptions"`
|
||||
}
|
||||
|
||||
type TextPresentation struct {
|
||||
TextAlign TextAlign `json:"textAlign"`
|
||||
VerticalAlign VerticalAlign `json:"verticalAlign"`
|
||||
Background *string `json:"background,omitempty" validate:"omitempty,hexcolor"`
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// Panel common types
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -247,6 +265,13 @@ type Axes struct {
|
||||
IsLogScale bool `json:"isLogScale"`
|
||||
}
|
||||
|
||||
// HeaderOptions controls the panel card's header strip — the title/description
|
||||
// row above the panel content. Phrased as hide so the zero value shows the
|
||||
// header, matching every other panel kind.
|
||||
type HeaderOptions struct {
|
||||
Hide bool `json:"hide"`
|
||||
}
|
||||
|
||||
type BasicVisualization struct {
|
||||
TimePreference TimePreference `json:"timePreference"`
|
||||
}
|
||||
@@ -658,6 +683,118 @@ func (sg SpanGaps) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TextMode is how a text panel interprets its `text`. Only markdown is
|
||||
// rendered today; further modes (e.g. plain text, HTML) are expected.
|
||||
type TextMode struct{ valuer.String }
|
||||
|
||||
var TextModeMarkdown = TextMode{valuer.NewString("markdown")} // default
|
||||
|
||||
func (TextMode) Enum() []any {
|
||||
return []any{TextModeMarkdown}
|
||||
}
|
||||
|
||||
func (m TextMode) ValueOrDefault() string {
|
||||
if m.IsZero() {
|
||||
return TextModeMarkdown.StringValue()
|
||||
}
|
||||
return m.StringValue()
|
||||
}
|
||||
|
||||
func (m TextMode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.ValueOrDefault())
|
||||
}
|
||||
|
||||
func (m *TextMode) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid text mode: must be the string `markdown`")
|
||||
}
|
||||
tm := TextMode{valuer.NewString(v)}
|
||||
switch tm {
|
||||
case TextModeMarkdown:
|
||||
*m = tm
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid text mode %q: must be `markdown`", v)
|
||||
}
|
||||
}
|
||||
|
||||
type TextAlign struct{ valuer.String }
|
||||
|
||||
var (
|
||||
TextAlignLeft = TextAlign{valuer.NewString("left")} // default
|
||||
TextAlignCenter = TextAlign{valuer.NewString("center")}
|
||||
TextAlignRight = TextAlign{valuer.NewString("right")}
|
||||
)
|
||||
|
||||
func (TextAlign) Enum() []any {
|
||||
return []any{TextAlignLeft, TextAlignCenter, TextAlignRight}
|
||||
}
|
||||
|
||||
func (a TextAlign) ValueOrDefault() string {
|
||||
if a.IsZero() {
|
||||
return TextAlignLeft.StringValue()
|
||||
}
|
||||
return a.StringValue()
|
||||
}
|
||||
|
||||
func (a TextAlign) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.ValueOrDefault())
|
||||
}
|
||||
|
||||
func (a *TextAlign) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid text align: must be a string, one of `left`, `center`, or `right`")
|
||||
}
|
||||
val := TextAlign{valuer.NewString(v)}
|
||||
switch val {
|
||||
case TextAlignLeft, TextAlignCenter, TextAlignRight:
|
||||
*a = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid text align %q: must be `left`, `center`, or `right`", v)
|
||||
}
|
||||
}
|
||||
|
||||
type VerticalAlign struct{ valuer.String }
|
||||
|
||||
var (
|
||||
VerticalAlignTop = VerticalAlign{valuer.NewString("top")} // default
|
||||
VerticalAlignCenter = VerticalAlign{valuer.NewString("center")}
|
||||
VerticalAlignBottom = VerticalAlign{valuer.NewString("bottom")}
|
||||
)
|
||||
|
||||
func (VerticalAlign) Enum() []any {
|
||||
return []any{VerticalAlignTop, VerticalAlignCenter, VerticalAlignBottom}
|
||||
}
|
||||
|
||||
func (a VerticalAlign) ValueOrDefault() string {
|
||||
if a.IsZero() {
|
||||
return VerticalAlignTop.StringValue()
|
||||
}
|
||||
return a.StringValue()
|
||||
}
|
||||
|
||||
func (a VerticalAlign) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.ValueOrDefault())
|
||||
}
|
||||
|
||||
func (a *VerticalAlign) UnmarshalJSON(data []byte) error {
|
||||
var v string
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid vertical align: must be a string, one of `top`, `center`, or `bottom`")
|
||||
}
|
||||
val := VerticalAlign{valuer.NewString(v)}
|
||||
switch val {
|
||||
case VerticalAlignTop, VerticalAlignCenter, VerticalAlignBottom:
|
||||
*a = val
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid vertical align %q: must be `top`, `center`, or `bottom`", v)
|
||||
}
|
||||
}
|
||||
|
||||
type PrecisionOption struct{ valuer.String }
|
||||
|
||||
var (
|
||||
|
||||
Reference in New Issue
Block a user