Compare commits

..

2 Commits

Author SHA1 Message Date
Abhi Kumar
4e7ec49ce0 fix(dashboard-v2): correct formatting carry on panel-kind switch
Switching a panel's visualization kind carried formatting fields the target
kind's schema does not accept (e.g. `unit` into a Table, which only supports
`columnUnits` + `decimalPrecision`), so the save API rejected the spec.

Unify new-panel and kind-switch spec seeding into one per-section registry
(`buildPluginSpec`, SECTION_SEEDS): each section derives its plugin-spec slice
from the target kind's declared `controls` and optional context, so a carried
field is emitted only when the target kind actually supports it. Adding a
section now means one registry entry rather than editing two seeders.

Delete the redundant `buildDefaultPluginSpec` wrapper (it only forwarded to
`buildPluginSpec`); `getSwitchedPluginSpec` becomes a thin delegating wrapper.
2026-07-03 02:43:37 +05:30
Abhi Kumar
1d7dd377eb refactor(dashboard-v2): make ThresholdVariant a string enum
Replace the ThresholdVariant string-union with a string enum so panel
section configs reference named members (ThresholdVariant.LABEL/COMPARISON/
TABLE) instead of bare string literals, and update every call site.
2026-07-03 02:42:10 +05:30
17 changed files with 708 additions and 572 deletions

View File

@@ -1,116 +1,105 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../docs/readme-assets/signoz-hero-dark.png" width="700">
<source media="(prefers-color-scheme: light)" srcset="../docs/readme-assets/signoz-hero-light.png" width="700">
<img alt="SigNoz - Observability on Your Terms" src="../docs/readme-assets/signoz-hero-light.png" width="700">
</picture>
</p>
# Configuring Over Local
1. Docker
1. Without Docker
<p align="center">
<a href="https://github.com/SigNoz/signoz/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SigNoz/signoz"></a>
<a href="https://signoz.io/slack"><img alt="Slack community" src="https://img.shields.io/badge/slack-community-4A154B?logo=slack&logoColor=white"></a>
</p>
## With Docker
# SigNoz Frontend
**Building image**
React-based web interface for [SigNoz](https://signoz.io), the open-source observability platform.
``docker compose up`
/ This will also run
## Tech Stack
or
`docker build . -t tagname`
- **Framework:** React 18 + TypeScript
- **Build:** Vite
- **State:** React Query, Zustand, Redux Toolkit (legacy)
- **Styling:** CSS Modules, Ant Design (legacy)
- **Charts:** uPlot
- **Testing:** Jest
## Local Development Setup
1. Run SigNoz backend locally — see [Self-Host Docs](https://signoz.io/docs/install/self-host/)
2. Configure environment:
```bash
cp example.env .env
```
Key variables in `.env`:
```bash
# Backend API endpoint (required)
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
# Enable bundle analyzer (optional)
BUNDLE_ANALYSER="true"
```
3. Install and run:
```bash
pnpm install
pnpm dev
```
## Development
```bash
pnpm dev
```
Opens [http://localhost:3301](http://localhost:3301).
## Build
```bash
pnpm build
```
Output in `build/` folder.
## Bundle Size Analysis
Set in `.env`:
```bash
BUNDLE_ANALYSER="true"
```
Then run build:
```bash
pnpm build
```
Opens bundle analyzer visualization automatically.
## Testing
```bash
# Unit tests
pnpm test
# Type checking
pnpm tsgo --noEmit
```
## Linting
```bash
# Run all linters (oxlint + stylelint)
pnpm lint
```
## Project Structure
**Tag to remote url- Introduce versioning later on**
```
src/
├── api/ # API clients and react-query hooks
├── components/ # Shared UI components
├── container/ # Page-level containers
├── hooks/ # Custom React hooks
├── pages/ # Route pages
├── providers/ # React context providers
├── store/ # Redux store
└── types/ # TypeScript definitions
docker tag signoz/frontend:latest 7296823551/signoz:latest
```
## Contributing
```
docker compose up
```
See [CONTRIBUTING.md](../CONTRIBUTING.md) in the root repo.
## Without Docker
Follow the steps below
Questions? Join our [Slack community](https://signoz.io/slack).
1. ```git clone https://github.com/SigNoz/signoz.git && cd signoz/frontend```
1. change baseURL to ```<test environment URL>``` in file ```src/constants/env.ts```
1. ```pnpm install```
1. ```pnpm dev```
```Note: Please ping us in #contributing channel in our slack community and we will DM you with <test environment URL>```
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `pnpm start`
Runs the app in the development mode.\
Open [http://localhost:3301](http://localhost:3301) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `pnpm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `pnpm build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `pnpm eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `pnpm build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

View File

@@ -35,6 +35,7 @@ function Explorer(): JSX.Element {
handleRunQuery,
stagedQuery,
updateAllQueriesOperators,
handleSetQueryData,
currentQuery,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
@@ -66,6 +67,15 @@ function Explorer(): JSX.Element {
[updateAllQueriesOperators],
);
useEffect(() => {
handleSetQueryData(0, {
...initialQueryMeterWithType.builder.queryData[0],
source: 'meter',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(

View File

@@ -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);

View File

@@ -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 }}
/>
);
}

View File

@@ -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' },
]);
});
});
});

View File

@@ -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,
});
}

View File

@@ -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 },
];

View File

@@ -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 },
];

View File

@@ -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 },
];

View File

@@ -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 },
];

View File

@@ -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 =

View File

@@ -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({});
});
});

View File

@@ -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({});
});
});
});

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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,