mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-03 05:10:34 +01:00
Compare commits
2 Commits
main
...
fix/dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7ec49ce0 | ||
|
|
1d7dd377eb |
@@ -8,7 +8,7 @@ import {
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
type DashboardtypesThresholdWithLabelDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
import {
|
||||
AnyThreshold,
|
||||
ThresholdVariant,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
@@ -77,7 +77,7 @@ function ThresholdsSection({
|
||||
yAxisUnit,
|
||||
tableColumns = [],
|
||||
}: ThresholdsSectionProps): JSX.Element {
|
||||
const variant = controls?.variant ?? 'label';
|
||||
const variant = controls?.variant ?? ThresholdVariant.LABEL;
|
||||
const thresholds = value ?? [];
|
||||
// Which row is being edited, and whether it was just added (so Discard removes it).
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
type DashboardtypesComparisonThresholdDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AnyThreshold } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import {
|
||||
ThresholdVariant,
|
||||
type AnyThreshold,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import UnifiedThresholdsSection from '../ThresholdsSection';
|
||||
@@ -21,7 +24,7 @@ function ComparisonThresholdsSection(props: {
|
||||
value={props.value}
|
||||
onChange={props.onChange as (next: AnyThreshold[]) => void}
|
||||
yAxisUnit={props.yAxisUnit}
|
||||
controls={{ variant: 'comparison' }}
|
||||
controls={{ variant: ThresholdVariant.COMPARISON }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
@@ -28,20 +26,21 @@ function specWith(pluginSpec: unknown): DashboardtypesPanelSpecDTO {
|
||||
} as unknown as DashboardtypesPanelSpecDTO;
|
||||
}
|
||||
|
||||
// Thin wrapper — only prove delegation; seeding rules are covered in buildPluginSpec.test.ts.
|
||||
describe('getSwitchedPluginSpec', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDefaultColumnsForSignal.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it('carries only unit + decimalPrecision when the new kind has a formatting section', () => {
|
||||
it("resolves the target kind's sections and carries the old spec through them", () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'formatting', controls: { unit: true, decimals: true } }],
|
||||
});
|
||||
const old = specWith({
|
||||
formatting: { unit: 'ms', decimalPrecision: 2, columnUnits: { A: 'bytes' } },
|
||||
axes: { logScale: true },
|
||||
sections: [
|
||||
{ kind: 'legend', controls: { position: true } },
|
||||
{ kind: 'formatting', controls: { unit: true, decimals: true } },
|
||||
],
|
||||
});
|
||||
const old = specWith({ formatting: { unit: 'ms', decimalPrecision: 2 } });
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
@@ -49,25 +48,12 @@ describe('getSwitchedPluginSpec', () => {
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(mockGetPanelDefinition).toHaveBeenCalledWith('signoz/TimeSeriesPanel');
|
||||
expect(result.legend?.position).toBe('bottom');
|
||||
expect(result.formatting).toStrictEqual({ unit: 'ms', decimalPrecision: 2 });
|
||||
// Type-specific config from the old kind is dropped.
|
||||
expect((result as { axes?: unknown }).axes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not carry formatting when the new kind has no formatting section', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
|
||||
const old = specWith({ formatting: { unit: 'ms' } });
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/ListPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.formatting).toBeUndefined();
|
||||
});
|
||||
|
||||
it('seeds List columns from the signal when switching into a List', () => {
|
||||
it('forwards the signal to seed List columns', () => {
|
||||
const columns = [{ name: 'body' }];
|
||||
mockDefaultColumnsForSignal.mockReturnValue(columns);
|
||||
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
|
||||
@@ -83,155 +69,4 @@ describe('getSwitchedPluginSpec', () => {
|
||||
);
|
||||
expect(result.selectFields).toBe(columns);
|
||||
});
|
||||
|
||||
it('includes the kind section defaults (e.g. legend position)', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'legend', controls: { position: true } }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
specWith({}),
|
||||
'signoz/PieChartPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.legend?.position).toBe('bottom');
|
||||
});
|
||||
|
||||
describe('thresholds', () => {
|
||||
it('does not carry thresholds when the new kind has no thresholds section', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({ sections: [{ kind: 'columns' }] });
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/ListPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('carries thresholds verbatim within the label variant (color/value/unit/label)', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'label' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/BarChartPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('remaps label thresholds into the comparison variant, defaulting operator + format', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'comparison' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/NumberPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
// The label is dropped; operator/format are seeded so the threshold can match.
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('remaps comparison thresholds into the table variant, keeping operator/format and seeding a column', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'table' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TablePanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
columnName: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops the table-only columnName when remapping into the label variant', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: { variant: 'label' } }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: 'p99',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TimeSeriesPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([{ value: 80, color: '#F1575F' }]);
|
||||
});
|
||||
|
||||
it('defaults the variant to label when the thresholds section omits controls', () => {
|
||||
mockGetPanelDefinition.mockReturnValue({
|
||||
sections: [{ kind: 'thresholds', controls: {} }],
|
||||
});
|
||||
const old = specWith({
|
||||
thresholds: [{ value: 80, color: '#F1575F', label: 'warn' }],
|
||||
});
|
||||
|
||||
const result = getSwitchedPluginSpec(
|
||||
old,
|
||||
'signoz/TimeSeriesPanel',
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
);
|
||||
|
||||
expect(result.thresholds).toStrictEqual([
|
||||
{ value: 80, color: '#F1575F', label: 'warn' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,149 +1,27 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
type TelemetrytypesSignalDTO,
|
||||
type TelemetrytypesTelemetryFieldKeyDTO,
|
||||
import type {
|
||||
DashboardtypesPanelSpecDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
|
||||
import type { PanelKind } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelKind';
|
||||
import {
|
||||
type AnyThreshold,
|
||||
type PanelFormattingSlice,
|
||||
type SectionConfig,
|
||||
SectionKind,
|
||||
type ThresholdVariant,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/types/sections';
|
||||
import {
|
||||
buildDefaultPluginSpec,
|
||||
type DefaultPluginSpec,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/buildDefaultPluginSpec';
|
||||
buildPluginSpec,
|
||||
type SeededPluginSpec,
|
||||
} from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/buildPluginSpec';
|
||||
|
||||
import { defaultColumnsForSignal } from './ListColumnsEditor/selectFields';
|
||||
export type SwitchedPluginSpec = SeededPluginSpec;
|
||||
|
||||
/**
|
||||
* Plugin spec produced on a first-time switch to a new kind. A partial cross-section
|
||||
* of the per-kind spec union; the caller assigns it to `plugin.spec` (typed `unknown`)
|
||||
* at the boundary.
|
||||
*/
|
||||
export interface SwitchedPluginSpec extends DefaultPluginSpec {
|
||||
formatting?: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'>;
|
||||
selectFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
thresholds?: AnyThreshold[];
|
||||
}
|
||||
|
||||
/** Every field any threshold variant can hold; switching reads across shapes to remap. */
|
||||
interface AnyThresholdFields {
|
||||
color: string;
|
||||
value: number;
|
||||
unit?: string;
|
||||
operator?: DashboardtypesComparisonOperatorDTO;
|
||||
format?: DashboardtypesThresholdFormatDTO;
|
||||
columnName?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** The threshold variant a kind edits, or `undefined` when it has no Thresholds section. */
|
||||
function getThresholdVariant(
|
||||
sections: SectionConfig[],
|
||||
): ThresholdVariant | undefined {
|
||||
const section = sections.find(
|
||||
(s): s is Extract<SectionConfig, { kind: SectionKind.Thresholds }> =>
|
||||
s.kind === SectionKind.Thresholds,
|
||||
);
|
||||
return section ? (section.controls.variant ?? 'label') : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remaps a threshold to the target kind's variant: keeps the shared core (color, value,
|
||||
* unit) plus any cross-variant fields, and seeds the rest with the variant's defaults so
|
||||
* the carried threshold stays functional (a comparison/table threshold needs an operator
|
||||
* to match, a table threshold a column).
|
||||
*/
|
||||
function toThresholdVariant(
|
||||
source: AnyThresholdFields,
|
||||
variant: ThresholdVariant,
|
||||
): AnyThreshold {
|
||||
const core = {
|
||||
color: source.color,
|
||||
value: source.value,
|
||||
...(source.unit !== undefined && { unit: source.unit }),
|
||||
};
|
||||
if (variant === 'comparison') {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.text,
|
||||
};
|
||||
}
|
||||
if (variant === 'table') {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: source.columnName ?? '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...core,
|
||||
...(source.label !== undefined && { label: source.label }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the plugin spec for a first-visit switch to `newKind`: the kind's declared
|
||||
* section defaults (so the config pane opens populated, matching new-panel seeding) plus
|
||||
* the cross-kind config worth keeping — unit + decimal precision, and thresholds when the
|
||||
* new kind supports them (remapped to its variant). Switching into a List seeds the
|
||||
* current signal's default columns so the columns control isn't empty.
|
||||
*
|
||||
* Revisiting a kind restores its stashed spec instead, so this runs only on first visit.
|
||||
* Plugin spec for a first-visit switch to `newKind`: the kind's defaults plus the cross-kind
|
||||
* config each section carries from `oldSpec`. Revisiting a kind restores its stash instead.
|
||||
*/
|
||||
export function getSwitchedPluginSpec(
|
||||
oldSpec: DashboardtypesPanelSpecDTO,
|
||||
newKind: PanelKind,
|
||||
signal: TelemetrytypesSignalDTO,
|
||||
): SwitchedPluginSpec {
|
||||
const sections = getPanelDefinition(newKind).sections;
|
||||
const result: SwitchedPluginSpec = buildDefaultPluginSpec(sections);
|
||||
|
||||
if (sections.some((section) => section.kind === SectionKind.Formatting)) {
|
||||
const oldFormatting = (
|
||||
oldSpec.plugin.spec as {
|
||||
formatting?: PanelFormattingSlice;
|
||||
}
|
||||
).formatting;
|
||||
const carried: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'> = {
|
||||
...(oldFormatting?.unit !== undefined && { unit: oldFormatting.unit }),
|
||||
...(oldFormatting?.decimalPrecision !== undefined && {
|
||||
decimalPrecision: oldFormatting.decimalPrecision,
|
||||
}),
|
||||
};
|
||||
if (Object.keys(carried).length > 0) {
|
||||
result.formatting = carried;
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.some((section) => section.kind === SectionKind.Columns)) {
|
||||
const columns = defaultColumnsForSignal(signal);
|
||||
if (columns.length > 0) {
|
||||
result.selectFields = columns;
|
||||
}
|
||||
}
|
||||
|
||||
const thresholdVariant = getThresholdVariant(sections);
|
||||
if (thresholdVariant) {
|
||||
const oldThresholds = (
|
||||
oldSpec.plugin.spec as {
|
||||
thresholds?: AnyThreshold[] | null;
|
||||
}
|
||||
).thresholds;
|
||||
if (oldThresholds && oldThresholds.length > 0) {
|
||||
result.thresholds = oldThresholds.map((threshold) =>
|
||||
toThresholdVariant(threshold as AnyThresholdFields, thresholdVariant),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return buildPluginSpec(getPanelDefinition(newKind).sections, {
|
||||
oldSpec,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
type SectionConfig,
|
||||
} from '../../types/sections';
|
||||
|
||||
// Bar stacking lives in `visualization.stackedBarChart`, so it's a `visualization`
|
||||
// control, not `chartAppearance`. fillSpans is TimeSeries-only, so Bar omits it (V1 parity).
|
||||
@@ -10,6 +14,9 @@ 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 } },
|
||||
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
|
||||
{
|
||||
kind: SectionKind.Thresholds,
|
||||
controls: { variant: ThresholdVariant.LABEL },
|
||||
},
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
type SectionConfig,
|
||||
} from '../../types/sections';
|
||||
|
||||
export const sections: SectionConfig[] = [
|
||||
{
|
||||
@@ -6,6 +10,9 @@ export const sections: SectionConfig[] = [
|
||||
controls: { switchPanelKind: true, timePreference: true },
|
||||
},
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
{ kind: SectionKind.Thresholds, controls: { variant: 'comparison' } },
|
||||
{
|
||||
kind: SectionKind.Thresholds,
|
||||
controls: { variant: ThresholdVariant.COMPARISON },
|
||||
},
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
type SectionConfig,
|
||||
} from '../../types/sections';
|
||||
|
||||
// A table panel renders one scalar result (the V5 backend joins every query into a
|
||||
// single column set). It exposes the per-panel time scope, formatting (decimals +
|
||||
@@ -12,6 +16,9 @@ export const sections: SectionConfig[] = [
|
||||
kind: SectionKind.Formatting,
|
||||
controls: { decimals: true, columnUnits: true },
|
||||
},
|
||||
{ kind: SectionKind.Thresholds, controls: { variant: 'table' } },
|
||||
{
|
||||
kind: SectionKind.Thresholds,
|
||||
controls: { variant: ThresholdVariant.TABLE },
|
||||
},
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
type SectionConfig,
|
||||
} from '../../types/sections';
|
||||
|
||||
export const sections: SectionConfig[] = [
|
||||
{
|
||||
@@ -18,6 +22,9 @@ export const sections: SectionConfig[] = [
|
||||
spanGaps: true,
|
||||
},
|
||||
},
|
||||
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
|
||||
{
|
||||
kind: SectionKind.Thresholds,
|
||||
controls: { variant: ThresholdVariant.LABEL },
|
||||
},
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
|
||||
@@ -58,7 +58,11 @@ export enum SectionKind {
|
||||
* - `comparison` — value crosses an operator → recolor (Number)
|
||||
* - `table` — per-column comparison (Table)
|
||||
*/
|
||||
export type ThresholdVariant = 'label' | 'comparison' | 'table';
|
||||
export enum ThresholdVariant {
|
||||
LABEL = 'label',
|
||||
COMPARISON = 'comparison',
|
||||
TABLE = 'table',
|
||||
}
|
||||
|
||||
/** Union of every threshold element shape stored under `plugin.spec.thresholds`. */
|
||||
export type AnyThreshold =
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
DashboardtypesFillModeDTO,
|
||||
DashboardtypesLegendPositionDTO,
|
||||
DashboardtypesLineInterpolationDTO,
|
||||
DashboardtypesLineStyleDTO,
|
||||
DashboardtypesTimePreferenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { sections as barSections } from '../../kinds/BarChartPanel/sections';
|
||||
import { sections as histogramSections } from '../../kinds/HistogramPanel/sections';
|
||||
import { sections as listSections } from '../../kinds/ListPanel/sections';
|
||||
import { sections as timeSeriesSections } from '../../kinds/TimeSeriesPanel/sections';
|
||||
import { SectionKind, type SectionConfig } from '../../types/sections';
|
||||
import { buildDefaultPluginSpec } from '../buildDefaultPluginSpec';
|
||||
|
||||
describe('buildDefaultPluginSpec', () => {
|
||||
it('seeds the TimeSeries dropdowns/segmented controls with their renderer defaults', () => {
|
||||
expect(buildDefaultPluginSpec(timeSeriesSections)).toStrictEqual({
|
||||
visualization: {
|
||||
timePreference: DashboardtypesTimePreferenceDTO.global_time,
|
||||
},
|
||||
legend: { position: DashboardtypesLegendPositionDTO.bottom },
|
||||
chartAppearance: {
|
||||
lineStyle: DashboardtypesLineStyleDTO.solid,
|
||||
lineInterpolation: DashboardtypesLineInterpolationDTO.spline,
|
||||
fillMode: DashboardtypesFillModeDTO.none,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('omits chartAppearance for a kind that does not declare it (Bar)', () => {
|
||||
expect(buildDefaultPluginSpec(barSections)).toStrictEqual({
|
||||
visualization: {
|
||||
timePreference: DashboardtypesTimePreferenceDTO.global_time,
|
||||
},
|
||||
legend: { position: DashboardtypesLegendPositionDTO.bottom },
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds only the legend for Histogram (no visualization section)', () => {
|
||||
expect(buildDefaultPluginSpec(histogramSections)).toStrictEqual({
|
||||
legend: { position: DashboardtypesLegendPositionDTO.bottom },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty spec for a kind with no seeded controls (List)', () => {
|
||||
expect(buildDefaultPluginSpec(listSections)).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('does not seed controls that already show a clear default', () => {
|
||||
// `axes` and `formatting` stay unset — their empty state is the chart default.
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
{ kind: SectionKind.Thresholds, controls: { variant: 'label' } },
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
expect(buildDefaultPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('only seeds the legend position when the kind exposes that control', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Legend, controls: { colors: true } },
|
||||
];
|
||||
expect(buildDefaultPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,328 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
DashboardtypesFillModeDTO,
|
||||
DashboardtypesLegendPositionDTO,
|
||||
DashboardtypesLineInterpolationDTO,
|
||||
DashboardtypesLineStyleDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
DashboardtypesTimePreferenceDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { defaultColumnsForSignal } from '../../../PanelEditor/ListColumnsEditor/selectFields';
|
||||
import { sections as listSections } from '../../kinds/ListPanel/sections';
|
||||
import { sections as timeSeriesSections } from '../../kinds/TimeSeriesPanel/sections';
|
||||
import {
|
||||
SectionKind,
|
||||
ThresholdVariant,
|
||||
type SectionConfig,
|
||||
} from '../../types/sections';
|
||||
import { buildPluginSpec } from '../buildPluginSpec';
|
||||
|
||||
jest.mock('../../../PanelEditor/ListColumnsEditor/selectFields', () => ({
|
||||
defaultColumnsForSignal: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockDefaultColumnsForSignal =
|
||||
defaultColumnsForSignal as unknown as jest.Mock;
|
||||
|
||||
/** A panel spec carrying the plugin.spec a seed reads; the rest of the shape is irrelevant. */
|
||||
function oldSpecWith(pluginSpec: unknown): DashboardtypesPanelSpecDTO {
|
||||
return {
|
||||
display: { name: 'Panel' },
|
||||
plugin: { kind: 'signoz/TimeSeriesPanel', spec: pluginSpec },
|
||||
queries: [],
|
||||
} as unknown as DashboardtypesPanelSpecDTO;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDefaultColumnsForSignal.mockReturnValue([]);
|
||||
});
|
||||
|
||||
describe('buildPluginSpec', () => {
|
||||
describe('folding mechanism', () => {
|
||||
it('returns an empty spec for no sections', () => {
|
||||
expect(buildPluginSpec([])).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('seeds nothing for sections with no seed (Axes, Buckets, ContextLinks)', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Axes, controls: { minMax: true, logScale: true } },
|
||||
{ kind: SectionKind.Buckets, controls: { count: true, width: true } },
|
||||
{ kind: SectionKind.ContextLinks },
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('omits the key entirely when a seed returns undefined (never key: undefined)', () => {
|
||||
const result = buildPluginSpec([
|
||||
{ kind: SectionKind.Legend, controls: { colors: true } },
|
||||
]);
|
||||
|
||||
expect(result).toStrictEqual({});
|
||||
expect(result).not.toHaveProperty('legend');
|
||||
});
|
||||
|
||||
it('composes defaults and carried config from several sections in one pass', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Visualization,
|
||||
controls: { switchPanelKind: true, timePreference: true },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true } },
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
];
|
||||
const oldSpec = oldSpecWith({
|
||||
formatting: { unit: 'ms', decimalPrecision: 2 },
|
||||
});
|
||||
|
||||
expect(buildPluginSpec(sections, { oldSpec })).toStrictEqual({
|
||||
visualization: {
|
||||
timePreference: DashboardtypesTimePreferenceDTO.global_time,
|
||||
},
|
||||
legend: { position: DashboardtypesLegendPositionDTO.bottom },
|
||||
formatting: { unit: 'ms', decimalPrecision: 2 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('visualization / legend seeds', () => {
|
||||
it('seeds visualization global_time and legend bottom when those controls are on', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Visualization,
|
||||
controls: { switchPanelKind: true, timePreference: true },
|
||||
},
|
||||
{ kind: SectionKind.Legend, controls: { position: true } },
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({
|
||||
visualization: {
|
||||
timePreference: DashboardtypesTimePreferenceDTO.global_time,
|
||||
},
|
||||
legend: { position: DashboardtypesLegendPositionDTO.bottom },
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds neither when their defaulting controls are absent', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Visualization, controls: { switchPanelKind: true } },
|
||||
{ kind: SectionKind.Legend, controls: { colors: true } },
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('chartAppearance seed', () => {
|
||||
it('seeds only the declared defaulting controls', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.ChartAppearance,
|
||||
controls: { lineStyle: true, fillMode: true },
|
||||
},
|
||||
];
|
||||
expect(buildPluginSpec(sections).chartAppearance).toStrictEqual({
|
||||
lineStyle: DashboardtypesLineStyleDTO.solid,
|
||||
fillMode: DashboardtypesFillModeDTO.none,
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds nothing when only non-defaulting controls are declared (showPoints/spanGaps)', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.ChartAppearance,
|
||||
controls: { showPoints: true, spanGaps: true },
|
||||
},
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatting seed (carry, gated by controls)', () => {
|
||||
it('carries unit + decimalPrecision when the kind declares both', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
];
|
||||
const oldSpec = oldSpecWith({
|
||||
formatting: { unit: 'ms', decimalPrecision: 3 },
|
||||
});
|
||||
|
||||
expect(buildPluginSpec(sections, { oldSpec }).formatting).toStrictEqual({
|
||||
unit: 'ms',
|
||||
decimalPrecision: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('drops unit when the target kind does not declare it (TimeSeries → Table)', () => {
|
||||
// Table formatting has columnUnits + decimals only; carrying unit breaks the save.
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Formatting,
|
||||
controls: { decimals: true, columnUnits: true },
|
||||
},
|
||||
];
|
||||
const oldSpec = oldSpecWith({
|
||||
formatting: { unit: 'ms', decimalPrecision: 2 },
|
||||
});
|
||||
|
||||
expect(buildPluginSpec(sections, { oldSpec }).formatting).toStrictEqual({
|
||||
decimalPrecision: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('carries a decimalPrecision of 0 (falsy but defined) and omits missing fields', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Formatting, controls: { unit: true, decimals: true } },
|
||||
];
|
||||
const oldSpec = oldSpecWith({ formatting: { decimalPrecision: 0 } });
|
||||
|
||||
expect(buildPluginSpec(sections, { oldSpec }).formatting).toStrictEqual({
|
||||
decimalPrecision: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds no formatting on a new panel or when nothing supported is present', () => {
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Formatting,
|
||||
controls: { decimals: true, columnUnits: true },
|
||||
},
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({});
|
||||
expect(
|
||||
buildPluginSpec(sections, {
|
||||
oldSpec: oldSpecWith({ formatting: { unit: 'ms' } }),
|
||||
}),
|
||||
).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('columns seed', () => {
|
||||
it('seeds the signal default columns when a Columns section is present', () => {
|
||||
const columns = [{ name: 'timestamp' }, { name: 'body' }];
|
||||
mockDefaultColumnsForSignal.mockReturnValue(columns);
|
||||
|
||||
const result = buildPluginSpec([{ kind: SectionKind.Columns }], {
|
||||
signal: TelemetrytypesSignalDTO.traces,
|
||||
});
|
||||
|
||||
expect(mockDefaultColumnsForSignal).toHaveBeenCalledWith(
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
);
|
||||
expect(result.selectFields).toBe(columns);
|
||||
});
|
||||
|
||||
it('seeds nothing (and skips the lookup) when no signal is in context', () => {
|
||||
const result = buildPluginSpec([{ kind: SectionKind.Columns }]);
|
||||
|
||||
expect(mockDefaultColumnsForSignal).not.toHaveBeenCalled();
|
||||
expect(result).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('thresholds seed (variant remap)', () => {
|
||||
function switchThresholds(
|
||||
variant: ThresholdVariant | undefined,
|
||||
thresholds: unknown[],
|
||||
): unknown {
|
||||
const sections: SectionConfig[] = [
|
||||
{ kind: SectionKind.Thresholds, controls: { variant } },
|
||||
];
|
||||
return buildPluginSpec(sections, { oldSpec: oldSpecWith({ thresholds }) })
|
||||
.thresholds;
|
||||
}
|
||||
|
||||
it('keeps color/value/unit/label within the label variant (and defaults to label)', () => {
|
||||
expect(
|
||||
switchThresholds(undefined, [
|
||||
{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' },
|
||||
]),
|
||||
).toStrictEqual([
|
||||
{ value: 80, color: '#F1575F', unit: 'ms', label: 'warn' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('remaps label → comparison, seeding operator + format and dropping label', () => {
|
||||
expect(
|
||||
switchThresholds(ThresholdVariant.COMPARISON, [
|
||||
{ value: 80, color: '#F1575F', label: 'warn' },
|
||||
]),
|
||||
).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves existing operator/format when remapping comparison → table', () => {
|
||||
expect(
|
||||
switchThresholds(ThresholdVariant.TABLE, [
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
},
|
||||
]),
|
||||
).toStrictEqual([
|
||||
{
|
||||
value: 80,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.below,
|
||||
format: DashboardtypesThresholdFormatDTO.text,
|
||||
columnName: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops table-only operator/format/columnName when remapping table → label', () => {
|
||||
expect(
|
||||
switchThresholds(ThresholdVariant.LABEL, [
|
||||
{
|
||||
value: 0,
|
||||
color: '#F1575F',
|
||||
operator: DashboardtypesComparisonOperatorDTO.above,
|
||||
format: DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: 'p99',
|
||||
},
|
||||
]),
|
||||
).toStrictEqual([{ value: 0, color: '#F1575F' }]);
|
||||
});
|
||||
|
||||
it('seeds nothing for an empty or absent threshold list', () => {
|
||||
expect(switchThresholds(ThresholdVariant.LABEL, [])).toBeUndefined();
|
||||
const sections: SectionConfig[] = [
|
||||
{
|
||||
kind: SectionKind.Thresholds,
|
||||
controls: { variant: ThresholdVariant.LABEL },
|
||||
},
|
||||
];
|
||||
expect(buildPluginSpec(sections)).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// Integration against real kind configs — guards against a section-config regression.
|
||||
describe('per-kind defaults (real sections, no context)', () => {
|
||||
it('seeds the full TimeSeries default set', () => {
|
||||
expect(buildPluginSpec(timeSeriesSections)).toStrictEqual({
|
||||
visualization: {
|
||||
timePreference: DashboardtypesTimePreferenceDTO.global_time,
|
||||
},
|
||||
legend: { position: DashboardtypesLegendPositionDTO.bottom },
|
||||
chartAppearance: {
|
||||
lineStyle: DashboardtypesLineStyleDTO.solid,
|
||||
lineInterpolation: DashboardtypesLineInterpolationDTO.spline,
|
||||
fillMode: DashboardtypesFillModeDTO.none,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty spec for List (only switchPanelKind, nothing to seed)', () => {
|
||||
expect(buildPluginSpec(listSections)).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import {
|
||||
DashboardtypesFillModeDTO,
|
||||
DashboardtypesLegendPositionDTO,
|
||||
DashboardtypesLineInterpolationDTO,
|
||||
DashboardtypesLineStyleDTO,
|
||||
DashboardtypesTimePreferenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import {
|
||||
SectionKind,
|
||||
type SectionConfig,
|
||||
type SectionSpecMap,
|
||||
} from '../types/sections';
|
||||
|
||||
/**
|
||||
* Seeded plugin-spec slices, typed as canonical section slices so each value is
|
||||
* checked against its DTO. A partial cross-section, not any single kind's spec,
|
||||
* so the union cast stays localized to `createDefaultPanel`.
|
||||
*/
|
||||
export interface DefaultPluginSpec {
|
||||
visualization?: SectionSpecMap[SectionKind.Visualization];
|
||||
legend?: SectionSpecMap[SectionKind.Legend];
|
||||
chartAppearance?: SectionSpecMap[SectionKind.ChartAppearance];
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds per-kind config defaults derived from the kind's declared `sections` so the
|
||||
* config pane opens populated. Values equal the renderer fallbacks (display only).
|
||||
* Controls whose empty state already IS the default are left unset.
|
||||
*/
|
||||
export function buildDefaultPluginSpec(
|
||||
sections: SectionConfig[],
|
||||
): DefaultPluginSpec {
|
||||
const spec: DefaultPluginSpec = {};
|
||||
|
||||
sections.forEach((section) => {
|
||||
switch (section.kind) {
|
||||
case SectionKind.Visualization:
|
||||
if (section.controls.timePreference) {
|
||||
spec.visualization = {
|
||||
timePreference: DashboardtypesTimePreferenceDTO.global_time,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case SectionKind.Legend:
|
||||
if (section.controls.position) {
|
||||
spec.legend = { position: DashboardtypesLegendPositionDTO.bottom };
|
||||
}
|
||||
break;
|
||||
case SectionKind.ChartAppearance: {
|
||||
const chartAppearance: SectionSpecMap[SectionKind.ChartAppearance] = {};
|
||||
if (section.controls.lineStyle) {
|
||||
chartAppearance.lineStyle = DashboardtypesLineStyleDTO.solid;
|
||||
}
|
||||
if (section.controls.lineInterpolation) {
|
||||
chartAppearance.lineInterpolation =
|
||||
DashboardtypesLineInterpolationDTO.spline;
|
||||
}
|
||||
if (section.controls.fillMode) {
|
||||
chartAppearance.fillMode = DashboardtypesFillModeDTO.none;
|
||||
}
|
||||
if (Object.keys(chartAppearance).length > 0) {
|
||||
spec.chartAppearance = chartAppearance;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return spec;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
DashboardtypesComparisonOperatorDTO,
|
||||
DashboardtypesFillModeDTO,
|
||||
DashboardtypesLegendPositionDTO,
|
||||
DashboardtypesLineInterpolationDTO,
|
||||
DashboardtypesLineStyleDTO,
|
||||
type DashboardtypesPanelSpecDTO,
|
||||
DashboardtypesThresholdFormatDTO,
|
||||
DashboardtypesTimePreferenceDTO,
|
||||
type TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { defaultColumnsForSignal } from '../../PanelEditor/ListColumnsEditor/selectFields';
|
||||
import {
|
||||
type AnyThreshold,
|
||||
type PanelFormattingSlice,
|
||||
type SectionConfig,
|
||||
type SectionControls,
|
||||
SectionKind,
|
||||
type SectionSpecMap,
|
||||
ThresholdVariant,
|
||||
} from '../types/sections';
|
||||
|
||||
/** Cross-section of the per-kind spec union; assigned to `plugin.spec` (unknown) at the boundary. */
|
||||
export interface SeededPluginSpec {
|
||||
visualization?: SectionSpecMap[SectionKind.Visualization];
|
||||
legend?: SectionSpecMap[SectionKind.Legend];
|
||||
chartAppearance?: SectionSpecMap[SectionKind.ChartAppearance];
|
||||
formatting?: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'>;
|
||||
selectFields?: SectionSpecMap[SectionKind.Columns];
|
||||
thresholds?: AnyThreshold[];
|
||||
}
|
||||
|
||||
export interface SeedContext {
|
||||
/** Present only on a kind switch — the spec being switched away from, to carry config across. */
|
||||
oldSpec?: DashboardtypesPanelSpecDTO;
|
||||
signal?: TelemetrytypesSignalDTO;
|
||||
}
|
||||
|
||||
interface AnyThresholdFields {
|
||||
color: string;
|
||||
value: number;
|
||||
unit?: string;
|
||||
operator?: DashboardtypesComparisonOperatorDTO;
|
||||
format?: DashboardtypesThresholdFormatDTO;
|
||||
columnName?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** Remaps a threshold to the target variant, seeding the fields that variant needs to stay functional. */
|
||||
function toThresholdVariant(
|
||||
source: AnyThresholdFields,
|
||||
variant: ThresholdVariant,
|
||||
): AnyThreshold {
|
||||
const core = {
|
||||
color: source.color,
|
||||
value: source.value,
|
||||
...(source.unit !== undefined && { unit: source.unit }),
|
||||
};
|
||||
if (variant === ThresholdVariant.COMPARISON) {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.text,
|
||||
};
|
||||
}
|
||||
if (variant === ThresholdVariant.TABLE) {
|
||||
return {
|
||||
...core,
|
||||
operator: source.operator ?? DashboardtypesComparisonOperatorDTO.above,
|
||||
format: source.format ?? DashboardtypesThresholdFormatDTO.background,
|
||||
columnName: source.columnName ?? '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...core,
|
||||
...(source.label !== undefined && { label: source.label }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* How one section derives its plugin-spec slice on create/switch — the single place a section
|
||||
* declares this. Sections absent from `SECTION_SEEDS` seed nothing.
|
||||
*/
|
||||
interface SectionSeed {
|
||||
specKey: keyof SeededPluginSpec;
|
||||
seed: (controls: unknown, ctx: SeedContext) => unknown;
|
||||
}
|
||||
|
||||
const SECTION_SEEDS: Partial<Record<SectionKind, SectionSeed>> = {
|
||||
[SectionKind.Visualization]: {
|
||||
specKey: 'visualization',
|
||||
seed: (controls): SectionSpecMap[SectionKind.Visualization] | undefined => {
|
||||
const c = controls as SectionControls[SectionKind.Visualization];
|
||||
return c.timePreference
|
||||
? { timePreference: DashboardtypesTimePreferenceDTO.global_time }
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
[SectionKind.Legend]: {
|
||||
specKey: 'legend',
|
||||
seed: (controls): SectionSpecMap[SectionKind.Legend] | undefined => {
|
||||
const c = controls as SectionControls[SectionKind.Legend];
|
||||
return c.position
|
||||
? { position: DashboardtypesLegendPositionDTO.bottom }
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
[SectionKind.ChartAppearance]: {
|
||||
specKey: 'chartAppearance',
|
||||
seed: (controls): SectionSpecMap[SectionKind.ChartAppearance] | undefined => {
|
||||
const c = controls as SectionControls[SectionKind.ChartAppearance];
|
||||
const appearance: SectionSpecMap[SectionKind.ChartAppearance] = {};
|
||||
if (c.lineStyle) {
|
||||
appearance.lineStyle = DashboardtypesLineStyleDTO.solid;
|
||||
}
|
||||
if (c.lineInterpolation) {
|
||||
appearance.lineInterpolation = DashboardtypesLineInterpolationDTO.spline;
|
||||
}
|
||||
if (c.fillMode) {
|
||||
appearance.fillMode = DashboardtypesFillModeDTO.none;
|
||||
}
|
||||
return Object.keys(appearance).length > 0 ? appearance : undefined;
|
||||
},
|
||||
},
|
||||
[SectionKind.Formatting]: {
|
||||
specKey: 'formatting',
|
||||
seed: (
|
||||
controls,
|
||||
{ oldSpec },
|
||||
): Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'> | undefined => {
|
||||
const c = controls as SectionControls[SectionKind.Formatting];
|
||||
const old = (oldSpec?.plugin.spec as { formatting?: PanelFormattingSlice })
|
||||
?.formatting;
|
||||
// Carry a field only when the target kind declares it (e.g. Table has no `unit`),
|
||||
// else the save API rejects the spec.
|
||||
const carried: Pick<PanelFormattingSlice, 'unit' | 'decimalPrecision'> = {
|
||||
...(c.unit && old?.unit !== undefined && { unit: old.unit }),
|
||||
...(c.decimals &&
|
||||
old?.decimalPrecision !== undefined && {
|
||||
decimalPrecision: old.decimalPrecision,
|
||||
}),
|
||||
};
|
||||
return Object.keys(carried).length > 0 ? carried : undefined;
|
||||
},
|
||||
},
|
||||
[SectionKind.Columns]: {
|
||||
specKey: 'selectFields',
|
||||
seed: (
|
||||
_controls,
|
||||
{ signal },
|
||||
): SectionSpecMap[SectionKind.Columns] | undefined => {
|
||||
if (!signal) {
|
||||
return undefined;
|
||||
}
|
||||
const columns = defaultColumnsForSignal(signal);
|
||||
return columns.length > 0 ? columns : undefined;
|
||||
},
|
||||
},
|
||||
[SectionKind.Thresholds]: {
|
||||
specKey: 'thresholds',
|
||||
seed: (controls, { oldSpec }): AnyThreshold[] | undefined => {
|
||||
const c = controls as SectionControls[SectionKind.Thresholds];
|
||||
const variant = c.variant ?? ThresholdVariant.LABEL;
|
||||
const old = (oldSpec?.plugin.spec as { thresholds?: AnyThreshold[] | null })
|
||||
?.thresholds;
|
||||
if (!old || old.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return old.map((threshold) =>
|
||||
toThresholdVariant(threshold as AnyThresholdFields, variant),
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a kind's plugin spec from its declared `sections`: no context → per-kind defaults
|
||||
* (new panel); `{ oldSpec, signal }` → defaults plus the config each target section carries.
|
||||
*/
|
||||
export function buildPluginSpec(
|
||||
sections: SectionConfig[],
|
||||
ctx: SeedContext = {},
|
||||
): SeededPluginSpec {
|
||||
const spec: SeededPluginSpec = {};
|
||||
|
||||
sections.forEach((section) => {
|
||||
const entry = SECTION_SEEDS[section.kind];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
const controls = 'controls' in section ? section.controls : undefined;
|
||||
const value = entry.seed(controls, ctx);
|
||||
if (value !== undefined) {
|
||||
// specKey ↔ value correlation can't be proven across the lookup; one localized cast.
|
||||
(spec as Record<string, unknown>)[entry.specKey] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return spec;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { PanelKind } from './Panels/types/panelKind';
|
||||
import type { DefaultPluginSpec } from './Panels/utils/buildDefaultPluginSpec';
|
||||
import type { SeededPluginSpec } from './Panels/utils/buildPluginSpec';
|
||||
import type { GridItem } from './utils';
|
||||
|
||||
/**
|
||||
@@ -36,7 +36,7 @@ export function panelRef(panelId: string): string {
|
||||
*/
|
||||
export function createDefaultPanel(
|
||||
pluginKind: PanelKind,
|
||||
pluginSpec: DefaultPluginSpec = {},
|
||||
pluginSpec: SeededPluginSpec = {},
|
||||
queries: DashboardtypesQueryDTO[] = [],
|
||||
): DashboardtypesPanelDTO {
|
||||
return {
|
||||
|
||||
@@ -13,7 +13,7 @@ import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
|
||||
import { getPanelDefinition } from '../DashboardContainer/Panels/registry';
|
||||
import { buildDefaultPluginSpec } from '../DashboardContainer/Panels/utils/buildDefaultPluginSpec';
|
||||
import { buildPluginSpec } from '../DashboardContainer/Panels/utils/buildPluginSpec';
|
||||
import { buildDefaultQueries } from '../DashboardContainer/Panels/utils/buildDefaultQueries';
|
||||
import PanelEditorContainer from '../DashboardContainer/PanelEditor';
|
||||
import {
|
||||
@@ -49,7 +49,7 @@ function PanelEditorPage(): JSX.Element {
|
||||
newKind
|
||||
? createDefaultPanel(
|
||||
newKind,
|
||||
buildDefaultPluginSpec(getPanelDefinition(newKind)?.sections ?? []),
|
||||
buildPluginSpec(getPanelDefinition(newKind).sections),
|
||||
buildDefaultQueries(newKind),
|
||||
)
|
||||
: existingPanel,
|
||||
|
||||
Reference in New Issue
Block a user