Compare commits

..

2 Commits

Author SHA1 Message Date
aks07
1af45169d5 feat(quick-filters): scroll the filter sidebar and content independently
Adds QuickFiltersLayout, a bounded two-pane layout that renders QuickFilters
in a fixed-width sidebar and the page content in its own OverlayScrollbar,
and moves Traces, LLM Observability, API Monitoring, Exceptions and Meter
onto it. The sidebar is 280px on every page. Logs and Infra are unchanged.
2026-09-19 11:45:20 +05:30
aks07
0c2a874e07 feat(route-tab): scroll tab content inside the pane instead of the page
RouteTab now owns the antd Tabs height chain and wraps each pane in an
OverlayScrollbar, so the tab bar stays put and pages no longer need their
own .ant-tabs overrides. Module pages pass their class to RouteTab instead
of wrapping it in a div, which was the auto-height link that let tall
content grow the page.
2026-09-18 19:40:48 +05:30
34 changed files with 429 additions and 1020 deletions

View File

@@ -3552,79 +3552,6 @@ components:
hide:
type: boolean
type: object
DashboardtypesHeatmapAxes:
properties:
yScale:
$ref: '#/components/schemas/DashboardtypesHeatmapYScale'
type: object
DashboardtypesHeatmapChartAppearance:
properties:
colors:
$ref: '#/components/schemas/DashboardtypesHeatmapColors'
type: object
DashboardtypesHeatmapColorMode:
enum:
- palette
- opacity
type: string
DashboardtypesHeatmapColorScale:
enum:
- log
- sqrt
- linear
type: string
DashboardtypesHeatmapColors:
properties:
fill:
type: string
maxCount:
nullable: true
type: number
minCount:
nullable: true
type: number
mode:
$ref: '#/components/schemas/DashboardtypesHeatmapColorMode'
palette:
$ref: '#/components/schemas/DashboardtypesHeatmapPalette'
scale:
$ref: '#/components/schemas/DashboardtypesHeatmapColorScale'
steps:
type: integer
type: object
DashboardtypesHeatmapPalette:
enum:
- ice
- moss
- rust
- graphite
- ember
- lagoon
- orchid
- verdant
- lava
- beacon
type: string
DashboardtypesHeatmapPanelSpec:
properties:
axes:
$ref: '#/components/schemas/DashboardtypesHeatmapAxes'
chartAppearance:
$ref: '#/components/schemas/DashboardtypesHeatmapChartAppearance'
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
visualization:
$ref: '#/components/schemas/DashboardtypesBasicVisualization'
type: object
DashboardtypesHeatmapYScale:
enum:
- auto
- linear
- log
- symlog
type: string
DashboardtypesHistogramBuckets:
properties:
bucketCount:
@@ -3977,7 +3904,6 @@ components:
discriminator:
mapping:
signoz/BarChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
signoz/HeatmapPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
signoz/HistogramPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
signoz/NumberPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
@@ -3995,7 +3921,6 @@ components:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
type: object
DashboardtypesPanelPluginKind:
enum:
@@ -4007,7 +3932,6 @@ components:
- signoz/HistogramPanel
- signoz/ListPanel
- signoz/TextPanel
- signoz/HeatmapPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
properties:
@@ -4021,18 +3945,6 @@ components:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec:
properties:
kind:
enum:
- signoz/HeatmapPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesHeatmapPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec:
properties:
kind:

View File

@@ -2,6 +2,8 @@
display: flex;
flex-direction: row;
position: relative;
flex: 1;
min-height: 0;
.quick-filters-settings-container {
flex: 0 0 0;

View File

@@ -0,0 +1,33 @@
// The one `overflow: hidden` in the chain. Ancestors (RouteTab, AppLayout)
// only hand height down; each pane below owns its own scroll.
.layout {
display: flex;
flex: 1;
height: 100%;
min-height: 0;
overflow: hidden;
}
// Positioned so overlays (settings drawer) paint above the content pane
// without changing this pane's layout width.
.filters {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
overflow: visible;
z-index: 2;
}
// Bounded box for the OverlayScrollbar inside it (`.overlay-scrollbar` is
// `height: 100%`), which owns the scrolling.
.content {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}

View File

@@ -0,0 +1,59 @@
import { ComponentProps, ReactNode } from 'react';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { PartialOptions } from 'overlayscrollbars';
import QuickFilters from '../QuickFilters';
import styles from './QuickFiltersLayout.module.scss';
const CONTENT_SCROLLBAR_OPTIONS: PartialOptions = {
overflow: { x: 'hidden' },
};
// Same optionality as `<QuickFilters />` in JSX (honours its defaultProps).
type QuickFiltersElementProps = JSX.LibraryManagedAttributes<
typeof QuickFilters,
ComponentProps<typeof QuickFilters>
>;
export interface QuickFiltersLayoutProps {
quickFilterProps: QuickFiltersElementProps;
showFilters: boolean;
className?: string;
contentClassName?: string;
testId?: string;
children: ReactNode;
}
function QuickFiltersLayout({
quickFilterProps,
showFilters,
className,
contentClassName,
testId,
children,
}: QuickFiltersLayoutProps): JSX.Element {
return (
<div className={cx(styles.layout, className)} data-testid={testId}>
{showFilters && (
<aside
className={styles.filters}
data-testid="quick-filters-layout-filters"
>
<QuickFilters {...quickFilterProps} />
</aside>
)}
<section
className={cx(styles.content, contentClassName)}
data-testid="quick-filters-layout-content"
>
<OverlayScrollbar options={CONTENT_SCROLLBAR_OPTIONS}>
<div>{children}</div>
</OverlayScrollbar>
</section>
</div>
);
}
export default QuickFiltersLayout;

View File

@@ -0,0 +1,79 @@
import { render, screen } from 'tests/test-utils';
import { QuickFiltersSource } from '../../types';
import QuickFiltersLayout from '../QuickFiltersLayout';
jest.mock('../QuickFiltersLayout.module.scss', () => ({
__esModule: true,
default: {
layout: 'layout',
filters: 'filters',
content: 'content',
},
}));
jest.mock('../../QuickFilters', () => ({
__esModule: true,
default: ({ source }: { source: string }): JSX.Element => (
<div data-testid="quick-filters">{source}</div>
),
}));
const quickFilterProps = {
source: QuickFiltersSource.TRACES_EXPLORER,
handleFilterVisibilityChange: jest.fn(),
};
describe('QuickFiltersLayout', () => {
it('renders QuickFilters with the given props inside the filters pane', () => {
render(
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
<div>content</div>
</QuickFiltersLayout>,
);
const filtersPane = screen.getByTestId('quick-filters-layout-filters');
expect(filtersPane).toContainElement(screen.getByTestId('quick-filters'));
expect(screen.getByTestId('quick-filters')).toHaveTextContent(
QuickFiltersSource.TRACES_EXPLORER,
);
expect(screen.getByTestId('quick-filters-layout-content')).toHaveTextContent(
'content',
);
});
it('does not render the filters pane when showFilters is false', () => {
render(
<QuickFiltersLayout showFilters={false} quickFilterProps={quickFilterProps}>
<div>content</div>
</QuickFiltersLayout>,
);
expect(
screen.queryByTestId('quick-filters-layout-filters'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('quick-filters')).not.toBeInTheDocument();
expect(screen.getByText('content')).toBeInTheDocument();
});
it('merges classNames onto the root and content panes', () => {
render(
<QuickFiltersLayout
showFilters
quickFilterProps={quickFilterProps}
className="page-root"
contentClassName="page-content"
testId="page"
>
<div>content</div>
</QuickFiltersLayout>,
);
const root = screen.getByTestId('page');
expect(root).toHaveClass('layout', 'page-root');
expect(screen.getByTestId('quick-filters-layout-content')).toHaveClass(
'content',
'page-content',
);
});
});

View File

@@ -0,0 +1,38 @@
// Hands the parent's height down to the active pane and lets the pane scroll
// its own content, so TopNav and the tab bar stay put. Child combinators only
// (nested Tabs must not be caught).
.routeTab {
flex: 1;
min-height: 0;
}
.routeTab > :global(.ant-tabs-content-holder) {
display: flex;
flex-direction: column;
}
.routeTab > :global(.ant-tabs-content-holder) > :global(.ant-tabs-content) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.routeTab
> :global(.ant-tabs-content-holder)
> :global(.ant-tabs-content)
> :global(.ant-tabs-tabpane-active) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.routeTab
> :global(.ant-tabs-content-holder)
> :global(.ant-tabs-content)
> :global(.ant-tabs-tabpane-active)
> :global(.overlay-scrollbar) {
flex: 1;
min-height: 0;
}

View File

@@ -5,6 +5,11 @@ import { fireEvent, render, screen } from 'tests/test-utils';
import RouteTab from './index';
import { RouteTabProps } from './types';
jest.mock('./RouteTab.module.scss', () => ({
__esModule: true,
default: { routeTab: 'routeTab' },
}));
function DummyComponent1(): JSX.Element {
return <div>Dummy Component 1</div>;
}
@@ -74,6 +79,36 @@ describe('RouteTab component', () => {
expect(history.location.pathname).toBe('/tab2');
});
it('applies the layout class alongside a custom className', () => {
const history = createMemoryHistory();
const { container } = render(
<Router history={history}>
<RouteTab
history={history}
routes={testRoutes}
activeKey="Tab1"
className="custom-tabs"
/>
</Router>,
);
expect(container.querySelector('.ant-tabs')).toHaveClass(
'routeTab',
'custom-tabs',
);
});
it('renders the active tab content inside an overlay scrollbar', () => {
const history = createMemoryHistory();
const { container } = render(
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
</Router>,
);
expect(
container.querySelector('.ant-tabs-tabpane-active > .overlay-scrollbar'),
).toHaveTextContent('Dummy Component 1');
});
it('calls onChangeHandler on tab change', () => {
const onChangeHandler = jest.fn();
const history = createMemoryHistory();

View File

@@ -5,20 +5,32 @@ import {
useParams,
} from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import cx from 'classnames';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { RouteTabProps } from './types';
import styles from './RouteTab.module.scss';
interface Params {
[key: string]: string;
}
/**
* Each pane scrolls its own content inside an OverlayScrollbar, so the tab bar
* stays put. Mounted as the page root the pane is bounded to the viewport; inside
* a plain block wrapper the scroller is inert and the page scrolls as usual.
* Pane content that needs a bounded box must size itself with `height: 100%`
* (the scroller's viewport is block flow, so `flex: 1` has no effect there).
*/
function RouteTab({
routes,
activeKey,
onChangeHandler,
history,
showRightSection,
className,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
const params = useParams<Params>();
@@ -50,11 +62,16 @@ function RouteTab({
label: name,
key,
tabKey: route,
children: <Component />,
children: (
<OverlayScrollbar>
<Component />
</OverlayScrollbar>
),
}));
return (
<Tabs
className={cx(styles.routeTab, className)}
onChange={onChange}
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}

View File

@@ -1,23 +1,15 @@
.api-monitoring-page {
display: flex;
height: 100%;
.api-monitoring-explorer {
.api-quick-filters-header {
padding: 12px;
border-bottom: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
.api-quick-filter-left-section {
width: 0%;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
.api-quick-filters-header {
padding: 12px;
border-bottom: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
line-height: 18px;
}
font-size: 14px;
line-height: 18px;
}
.api-module-right-section {
@@ -161,16 +153,6 @@
}
}
}
&.filter-visible {
.api-quick-filter-left-section {
width: 260px;
}
.api-module-right-section {
width: calc(100% - 260px);
}
}
}
.no-filtered-domains-message-container {

View File

@@ -1,8 +1,7 @@
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
@@ -20,20 +19,21 @@ function Explorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className={cx('api-monitoring-page', 'filter-visible')}>
<section className="api-quick-filter-left-section">
<QuickFilters
className="qf-api-monitoring"
source={QuickFiltersSource.API_MONITORING}
signal={SignalType.API_MONITORING}
showFilterCollapse={false}
showQueryName={false}
handleFilterVisibilityChange={(): void => {}}
useFieldApis={quickFilterFieldApis}
/>
</section>
<QuickFiltersLayout
className="api-monitoring-explorer"
showFilters
quickFilterProps={{
className: 'qf-api-monitoring',
source: QuickFiltersSource.API_MONITORING,
signal: SignalType.API_MONITORING,
showFilterCollapse: false,
showQueryName: false,
handleFilterVisibilityChange: (): void => {},
useFieldApis: quickFilterFieldApis,
}}
>
<DomainList />
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>
);
}

View File

@@ -65,8 +65,6 @@
}
.trace-explorer-page {
display: flex;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
@@ -75,32 +73,8 @@
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
.filter {
width: 260px;
height: 100%;
min-height: 100vh;
border-right: 0px;
border: 1px solid var(--l1-border);
background-color: var(--l1-background);
> .ant-card-body {
padding: 0;
width: 258px;
}
}
.trace-explorer {
width: 100%;
background: var(--l1-background);
> .ant-card-body {
padding: 0;
}
border-color: var(--l1-border);
}
.trace-explorer.filters-expanded {
width: calc(100% - 260px);
}
}

View File

@@ -2,12 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
@@ -253,25 +251,20 @@ function Explorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div
<QuickFiltersLayout
className="trace-explorer-page"
data-testid="llm-observability-explorer"
testId="llm-observability-explorer"
showFilters={isOpen}
quickFilterProps={{
className: 'qf-traces-explorer',
source: QuickFiltersSource.TRACES_EXPLORER,
signal: SignalType.TRACES,
handleFilterVisibilityChange: (): void => {
setOpen(!isOpen);
},
}}
>
<Card className="filter" hidden={!isOpen}>
<QuickFilters
className="qf-traces-explorer"
source={QuickFiltersSource.TRACES_EXPLORER}
signal={SignalType.TRACES}
handleFilterVisibilityChange={(): void => {
setOpen(!isOpen);
}}
/>
</Card>
<div
className={cx('trace-explorer', {
'filters-expanded': isOpen,
})}
>
<div className="trace-explorer">
<div className="trace-explorer-header">
<Toolbar
showAutoRefresh
@@ -363,7 +356,7 @@ function Explorer(): JSX.Element {
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>
);
}

View File

@@ -1,18 +1,7 @@
.meter-explorer-container {
display: flex;
flex-direction: row;
.meter-explorer-quick-filters-section {
width: 280px;
border-right: 1px solid var(--l1-border);
&.hidden {
display: none;
}
}
.meter-explorer-content-section {
width: 100%;
// Clearance for the fixed ExplorerOptions bar.
padding-bottom: 80px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
@@ -83,14 +72,6 @@
}
}
}
&.quick-filters-open {
.meter-explorer-content-section {
width: calc(100% - 280px);
}
}
padding-bottom: 80px;
}
.dashboards-and-alerts-popover-container {

View File

@@ -3,9 +3,8 @@ import { useQueryClient } from 'react-query';
import * as Sentry from '@sentry/react';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
@@ -121,29 +120,21 @@ function Explorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div
className={cx('meter-explorer-container', {
'quick-filters-open': showQuickFilters,
})}
<QuickFiltersLayout
className="meter-explorer-container"
showFilters={showQuickFilters}
quickFilterProps={{
className: 'qf-meter-explorer',
source: QuickFiltersSource.METER_EXPLORER,
signal: SignalType.METER_EXPLORER,
showFilterCollapse: true,
showQueryName: false,
handleFilterVisibilityChange: (): void => {
setShowQuickFilters(!showQuickFilters);
},
useFieldApis: quickFilterFieldApis,
}}
>
<div
className={cx('meter-explorer-quick-filters-section', {
hidden: !showQuickFilters,
})}
>
<QuickFilters
className="qf-meter-explorer"
source={QuickFiltersSource.METER_EXPLORER}
signal={SignalType.METER_EXPLORER}
showFilterCollapse
showQueryName={false}
handleFilterVisibilityChange={(): void => {
setShowQuickFilters(!showQuickFilters);
}}
useFieldApis={quickFilterFieldApis}
/>
</div>
<div className="meter-explorer-content-section">
<div className="meter-explorer-explore-content">
<div className="explore-header">
@@ -196,7 +187,7 @@ function Explorer(): JSX.Element {
splitedQueries={splitedQueries}
/>
</div>
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>
);
}

View File

@@ -1,11 +1,4 @@
.all-errors-page {
display: flex;
height: 100%;
.all-errors-quick-filter-section {
width: 0%;
flex-shrink: 0;
}
.all-errors-right-section {
.right-toolbar-actions-container {
display: flex;
@@ -18,14 +11,4 @@
.ant-tabs {
margin: 0 8px;
}
&.filter-visible {
.all-errors-quick-filter-section {
width: 260px;
}
.all-errors-right-section {
width: calc(100% - 260px);
}
}
}

View File

@@ -5,13 +5,11 @@ import { Filter } from '@signozhq/icons';
import { Button, Tooltip } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import RouteTab from 'components/RouteTab';
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import ResourceAttributesFilterV2 from 'container/ResourceAttributeFilterV2/ResourceAttributesFilterV2';
@@ -59,63 +57,52 @@ function AllErrors(): JSX.Element {
const quickFilterFieldApis = useSignalFieldApis();
return (
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
{showFilters && (
<section className={cx('all-errors-quick-filter-section')}>
<QuickFilters
className="qf-exceptions"
source={QuickFiltersSource.EXCEPTIONS}
signal={SignalType.EXCEPTIONS}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={quickFilterFieldApis}
/>
</section>
)}
<section
className={cx(
'all-errors-right-section',
showFilters ? 'filter-visible' : '',
)}
>
<TypicalOverlayScrollbar>
<>
<Toolbar
showAutoRefresh={false}
leftActions={
!showFilters ? (
<Tooltip title="Show Filters">
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
<Filter size="md" />
</Button>
</Tooltip>
) : undefined
}
rightActions={
<div className="right-toolbar-actions-container">
<RightToolbarActions
onStageRunQuery={handleRunQuery}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
/>
<HeaderRightSection
enableAnnouncements={false}
enableShare
enableFeedback
/>
</div>
}
<QuickFiltersLayout
className="all-errors-page"
contentClassName="all-errors-right-section"
showFilters={showFilters}
quickFilterProps={{
className: 'qf-exceptions',
source: QuickFiltersSource.EXCEPTIONS,
signal: SignalType.EXCEPTIONS,
handleFilterVisibilityChange,
useFieldApis: quickFilterFieldApis,
}}
>
<Toolbar
showAutoRefresh={false}
leftActions={
!showFilters ? (
<Tooltip title="Show Filters">
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
<Filter size="md" />
</Button>
</Tooltip>
) : undefined
}
rightActions={
<div className="right-toolbar-actions-container">
<RightToolbarActions
onStageRunQuery={handleRunQuery}
isLoadingQueries={isLoadingQueries}
handleCancelQuery={handleCancelQuery}
/>
<ResourceAttributesFilterV2 />
<RouteTab
routes={routes}
activeKey={pathname}
history={history}
showRightSection={false}
<HeaderRightSection
enableAnnouncements={false}
enableShare
enableFeedback
/>
</>
</TypicalOverlayScrollbar>
</section>
</div>
</div>
}
/>
<ResourceAttributesFilterV2 />
<RouteTab
routes={routes}
activeKey={pathname}
history={history}
showRightSection={false}
/>
</QuickFiltersLayout>
);
}

View File

@@ -1,11 +1,4 @@
.api-monitoring-page {
flex: 1;
display: flex;
.ant-tabs {
flex: 1;
}
.ant-tabs-nav {
padding: 0 16px;
margin-bottom: 0px;
@@ -15,22 +8,6 @@
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;

View File

@@ -13,9 +13,12 @@ function ApiMonitoringPage(): JSX.Element {
const routes: TabRoutes[] = [Explorer];
return (
<div className="api-monitoring-page">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
<RouteTab
className="api-monitoring-page"
routes={routes}
activeKey={pathname}
history={history}
/>
);
}

View File

@@ -1,13 +1,4 @@
.infra-monitoring-module-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-nav {
padding: 0 8px;
margin-bottom: 0px;
@@ -17,22 +8,6 @@
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;

View File

@@ -13,8 +13,11 @@ export default function InfrastructureMonitoringPage(): JSX.Element {
const routes: TabRoutes[] = [Hosts, Kubernetes];
return (
<div className="infra-monitoring-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
<RouteTab
className="infra-monitoring-module-container"
routes={routes}
activeKey={pathname}
history={history}
/>
);
}

View File

@@ -1,16 +1,4 @@
.logs-module-container {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
.ant-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.ant-tabs-nav {
padding: 0 16px;
margin-bottom: 0px;
@@ -20,25 +8,6 @@
}
}
.ant-tabs-content-holder {
display: flex;
min-height: 0;
.ant-tabs-content {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;

View File

@@ -13,8 +13,11 @@ export default function LogsModulePage(): JSX.Element {
const routes: TabRoutes[] = [logsExplorer, logsPipelines, logSaveView];
return (
<div className="logs-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
<RouteTab
className="logs-module-container"
routes={routes}
activeKey={pathname}
history={history}
/>
);
}

View File

@@ -1,13 +1,4 @@
.messaging-queues-module-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-nav {
padding: 0 8px;
margin-bottom: 0px;
@@ -17,22 +8,6 @@
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;

View File

@@ -68,8 +68,11 @@ export default function MessagingQueuesMainPage(): JSX.Element {
];
return (
<div className="messaging-queues-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
<RouteTab
className="messaging-queues-module-container"
routes={routes}
activeKey={pathname}
history={history}
/>
);
}

View File

@@ -14,14 +14,13 @@ function MeterExplorerPage(): JSX.Element {
const routes: TabRoutes[] = [Meter, Explorer, Views];
return (
<div className="meter-explorer-page">
<RouteTab
routes={routes}
activeKey={pathname}
history={history}
defaultActiveKey={ROUTES.METER}
/>
</div>
<RouteTab
className="meter-explorer-page"
routes={routes}
activeKey={pathname}
history={history}
defaultActiveKey={ROUTES.METER}
/>
);
}

View File

@@ -1,13 +1,4 @@
.metrics-explorer-page {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-nav {
padding-left: 16px;
margin-bottom: 0px;
@@ -18,20 +9,7 @@
}
.ant-tabs-content-holder {
display: flex;
padding: 16px;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {

View File

@@ -42,9 +42,12 @@ function MetricsExplorerPage(): JSX.Element {
useShareBuilderUrl({ defaultValue: defaultQuery });
return (
<div className="metrics-explorer-page">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
<RouteTab
className="metrics-explorer-page"
routes={routes}
activeKey={pathname}
history={history}
/>
);
}

View File

@@ -65,8 +65,6 @@
}
.trace-explorer-page {
display: flex;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
@@ -75,32 +73,8 @@
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
.filter {
width: 260px;
height: 100%;
min-height: 100vh;
border-right: 0px;
border: 1px solid var(--l1-border);
background-color: var(--l1-background);
> .ant-card-body {
padding: 0;
width: 258px;
}
}
.trace-explorer {
width: 100%;
background: var(--l1-background);
> .ant-card-body {
padding: 0;
}
border-color: var(--l1-border);
}
.trace-explorer.filters-expanded {
width: calc(100% - 260px);
}
}

View File

@@ -2,12 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import WarningPopover from 'components/WarningPopover/WarningPopover';
@@ -261,23 +259,20 @@ function TracesExplorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="trace-explorer-page">
<Card className="filter" hidden={!isOpen}>
<QuickFilters
className="qf-traces-explorer"
source={QuickFiltersSource.TRACES_EXPLORER}
signal={SignalType.TRACES}
handleFilterVisibilityChange={(): void => {
setOpen(!isOpen);
}}
useFieldApis={quickFilterFieldApis}
/>
</Card>
<div
className={cx('trace-explorer', {
'filters-expanded': isOpen,
})}
>
<QuickFiltersLayout
className="trace-explorer-page"
showFilters={isOpen}
quickFilterProps={{
className: 'qf-traces-explorer',
source: QuickFiltersSource.TRACES_EXPLORER,
signal: SignalType.TRACES,
handleFilterVisibilityChange: (): void => {
setOpen(!isOpen);
},
useFieldApis: quickFilterFieldApis,
}}
>
<div className="trace-explorer">
<div className="trace-explorer-header">
<Toolbar
showAutoRefresh
@@ -369,7 +364,7 @@ function TracesExplorer(): JSX.Element {
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>
);
}

View File

@@ -25,16 +25,15 @@ function TracesModulePage(): JSX.Element {
};
return (
<div className="traces-module-container">
<RouteTab
routes={routes}
activeKey={
pathname.includes(ROUTES.TRACES_FUNNELS) ? ROUTES.TRACES_FUNNELS : pathname
}
history={history}
onChangeHandler={handleTabChange}
/>
</div>
<RouteTab
className="traces-module-container"
routes={routes}
activeKey={
pathname.includes(ROUTES.TRACES_FUNNELS) ? ROUTES.TRACES_FUNNELS : pathname
}
history={history}
onChangeHandler={handleTabChange}
/>
);
}

View File

@@ -8,7 +8,6 @@ import (
"testing"
"github.com/SigNoz/signoz/pkg/errors"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/perses/spec/go/dashboard"
"github.com/stretchr/testify/assert"
@@ -525,149 +524,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
}
}
// TestHeatmapPanelQueryKinds pins the panel allowlist to what validateHeatmap
// accepts in querybuildertypesv5: everything but a trace operator.
func TestHeatmapPanelQueryKinds(t *testing.T) {
testCases := []struct {
description string
queryPluginKind string
queryPluginSpec string
expectedAllowed bool
}{
{
description: "a metrics builder query is allowed",
queryPluginKind: "signoz/BuilderQuery",
queryPluginSpec: `{"name": "A", "signal": "metrics", "aggregations": [
{"metricName": "http.server.request.duration", "timeAggregation": "increase", "spaceAggregation": "sum"}
]}`,
expectedAllowed: true,
},
{
description: "a promql query is allowed",
queryPluginKind: "signoz/PromQLQuery",
queryPluginSpec: `{"name": "A", "query": "sum by (le) (increase(signoz_latency_bucket[5m]))"}`,
expectedAllowed: true,
},
{
description: "a clickhouse query is allowed",
queryPluginKind: "signoz/ClickHouseSQL",
queryPluginSpec: `{"name": "A", "query": "SELECT ts, bucket, value FROM cells"}`,
expectedAllowed: true,
},
{
description: "a formula is allowed",
queryPluginKind: "signoz/Formula",
queryPluginSpec: `{"name": "F1", "expression": "A / B"}`,
expectedAllowed: true,
},
{
description: "a composite query is allowed, since a formula needs its disabled inputs alongside it",
queryPluginKind: "signoz/CompositeQuery",
queryPluginSpec: `{"queries": [
{"type": "builder_query", "spec": {"name": "A", "signal": "metrics", "disabled": true, "aggregations": [
{"metricName": "http.server.request.duration", "timeAggregation": "increase", "spaceAggregation": "sum"}
]}},
{"type": "builder_formula", "spec": {"name": "F1", "expression": "A * 2"}}
]}`,
expectedAllowed: true,
},
{
description: "a trace operator is refused",
queryPluginKind: "signoz/TraceOperator",
queryPluginSpec: `{"name": "T1", "expression": "A => B"}`,
expectedAllowed: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
data := fmt.Sprintf(`{
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"links": [],
"plugin": {"kind": "signoz/HeatmapPanel", "spec": {}},
"queries": [{
"kind": "heatmap",
"spec": {
"plugin": {"kind": %q, "spec": %s}
}
}]
}
}
},
"links": [],
"layouts": []
}`, testCase.queryPluginKind, testCase.queryPluginSpec)
_, err := unmarshalDashboard([]byte(data))
if testCase.expectedAllowed {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.Contains(t, err.Error(), "is not supported by panel kind")
})
}
}
func TestValidateHeatmapDashboard(t *testing.T) {
data, err := os.ReadFile("testdata/perses_heatmap_panel.json")
require.NoError(t, err, "reading example file")
spec, err := unmarshalDashboard(data)
require.NoError(t, err, "unmarshal and validate failed")
require.IsType(t, &HeatmapPanelSpec{}, spec.Panels["p1"].Spec.Plugin.Spec)
panelSpec := spec.Panels["p1"].Spec.Plugin.Spec.(*HeatmapPanelSpec)
assert.Equal(t, "log", panelSpec.Axes.YScale.ValueOrDefault())
assert.Equal(t, "ember", panelSpec.ChartAppearance.Colors.Palette.ValueOrDefault())
assert.Equal(t, "sqrt", panelSpec.ChartAppearance.Colors.Scale.ValueOrDefault())
assert.Equal(t, 8, panelSpec.ChartAppearance.Colors.Steps)
dashboard := &DashboardV2{Spec: *spec}
request, err := dashboard.GetPanelQuery(1, 2, "p1")
require.NoError(t, err, "building the panel's query failed")
assert.Equal(t, qb.RequestTypeHeatmap, request.RequestType)
require.Len(t, request.CompositeQuery.Queries, 3)
numerator, ok := request.CompositeQuery.Queries[0].Spec.(qb.QueryBuilderQuery[qb.MetricAggregation])
require.True(t, ok, "expected a metrics builder query")
assert.True(t, numerator.Disabled)
require.NotNil(t, numerator.BucketOptions)
require.IsType(t, qb.LogBucketsSpec{}, numerator.BucketOptions.Spec)
assert.Equal(t, 4, *numerator.BucketOptions.Spec.(qb.LogBucketsSpec).Scale)
denominator, ok := request.CompositeQuery.Queries[1].Spec.(qb.QueryBuilderQuery[qb.MetricAggregation])
require.True(t, ok, "expected a metrics builder query")
assert.True(t, denominator.Disabled)
require.NotNil(t, denominator.BucketOptions)
require.IsType(t, qb.LinearBucketsSpec{}, denominator.BucketOptions.Spec)
assert.Equal(t, float64(1000), denominator.BucketOptions.Spec.(qb.LinearBucketsSpec).MaxValue)
formula, ok := request.CompositeQuery.Queries[2].Spec.(qb.QueryBuilderFormula)
require.True(t, ok, "expected a formula")
require.NotNil(t, formula.BucketOptions)
assert.Equal(t, qb.BucketsKindLog, formula.BucketOptions.Kind)
require.IsType(t, qb.LogBucketsSpec{}, formula.BucketOptions.Spec)
assert.Equal(t, 2, *formula.BucketOptions.Spec.(qb.LogBucketsSpec).Scale)
require.NoError(t, request.Validate(), "the request built from the panel is not a valid heatmap request")
// the panel read back out of storage draws the same heatmap
stored, err := json.Marshal(spec)
require.NoError(t, err, "marshal dashboard failed")
reread, err := unmarshalDashboard(stored)
require.NoError(t, err, "the stored dashboard does not validate")
rereadRequest, err := (&DashboardV2{Spec: *reread}).GetPanelQuery(1, 2, "p1")
require.NoError(t, err, "building the stored panel's query failed")
assert.Equal(t, request, rereadRequest)
}
func TestInvalidateOneInvalidPanel(t *testing.T) {
data := []byte(`{
"variables": [],

View File

@@ -36,7 +36,6 @@ func (PanelPlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
string(PanelKindHistogram): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec"),
string(PanelKindList): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec"),
string(PanelKindText): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec"),
string(PanelKindHeatmap): schemaRef("DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec"),
})
}
@@ -68,7 +67,6 @@ func (PanelPlugin) JSONSchemaOneOf() []any {
PanelPluginVariant[HistogramPanelSpec]{Kind: string(PanelKindHistogram)},
PanelPluginVariant[ListPanelSpec]{Kind: string(PanelKindList)},
PanelPluginVariant[TextPanelSpec]{Kind: string(PanelKindText)},
PanelPluginVariant[HeatmapPanelSpec]{Kind: string(PanelKindHeatmap)},
}
}
@@ -233,7 +231,6 @@ var (
PanelKindHistogram: func() any { return new(HistogramPanelSpec) },
PanelKindList: func() any { return new(ListPanelSpec) },
PanelKindText: func() any { return new(TextPanelSpec) },
PanelKindHeatmap: func() any { return new(HeatmapPanelSpec) },
}
queryPluginSpecs = map[QueryPluginKind]func() any{
QueryKindBuilder: func() any { return new(BuilderQuerySpec) },
@@ -257,7 +254,6 @@ var (
PanelKindTable: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindClickHouseSQL},
PanelKindList: {QueryKindBuilder},
PanelKindText: {},
PanelKindHeatmap: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindPromQL, QueryKindClickHouseSQL},
}
)

View File

@@ -174,11 +174,10 @@ const (
PanelKindHistogram PanelPluginKind = "signoz/HistogramPanel"
PanelKindList PanelPluginKind = "signoz/ListPanel"
PanelKindText PanelPluginKind = "signoz/TextPanel"
PanelKindHeatmap PanelPluginKind = "signoz/HeatmapPanel"
)
func (PanelPluginKind) Enum() []any {
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText, PanelKindHeatmap}
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList, PanelKindText}
}
func (k PanelPluginKind) rendersWithoutQuery() bool {
@@ -243,56 +242,6 @@ type ListPanelSpec struct {
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitzero" validate:"dive"`
}
type HeatmapPanelSpec struct {
Visualization BasicVisualization `json:"visualization"`
Formatting PanelFormatting `json:"formatting"`
Axes HeatmapAxes `json:"axes"`
Legend Legend `json:"legend"`
ChartAppearance HeatmapChartAppearance `json:"chartAppearance"`
}
// HeatmapAxes carries only the Y scale. The shared Axes type models a value
// axis with soft bounds, where a heatmap's Y axis is the bucket boundaries the
// response already fixed.
type HeatmapAxes struct {
YScale HeatmapYScale `json:"yScale"`
}
type HeatmapChartAppearance struct {
Colors HeatmapColors `json:"colors"`
}
type HeatmapColors struct {
Mode HeatmapColorMode `json:"mode"`
Palette HeatmapPalette `json:"palette"`
Scale HeatmapColorScale `json:"scale"`
Steps int `json:"steps" validate:"omitempty,min=2,max=128"`
// MinCount and MaxCount clamp the colour scale; nil derives them from the
// grid, 0 and the highest count in it.
MinCount *float64 `json:"minCount"`
MaxCount *float64 `json:"maxCount"`
// Fill applies in opacity mode; empty means the selected group's legend colour.
Fill string `json:"fill"`
}
func (c *HeatmapColors) UnmarshalJSON(data []byte) error {
type alias HeatmapColors
var tmp alias
if err := json.Unmarshal(data, &tmp); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid heatmap colors")
}
*c = HeatmapColors(tmp)
return c.validate()
}
func (c HeatmapColors) validate() error {
if c.MinCount != nil && c.MaxCount != nil && *c.MinCount > *c.MaxCount {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput,
"heatmap colors.minCount %v is greater than colors.maxCount %v", *c.MinCount, *c.MaxCount)
}
return nil
}
type TextPanelSpec struct {
Mode TextMode `json:"mode"`
Text string `json:"text"`
@@ -897,168 +846,3 @@ func (p *PrecisionOption) UnmarshalJSON(data []byte) error {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid precision option %q: must be `0`, `1`, `2`, `3`, `4`, or `full`", v)
}
}
type HeatmapColorMode struct{ valuer.String }
var (
HeatmapColorModePalette = HeatmapColorMode{valuer.NewString("palette")} // default
HeatmapColorModeOpacity = HeatmapColorMode{valuer.NewString("opacity")}
)
func (HeatmapColorMode) Enum() []any {
return []any{HeatmapColorModePalette, HeatmapColorModeOpacity}
}
func (m HeatmapColorMode) ValueOrDefault() string {
if m.IsZero() {
return HeatmapColorModePalette.StringValue()
}
return m.StringValue()
}
func (m HeatmapColorMode) MarshalJSON() ([]byte, error) {
return json.Marshal(m.ValueOrDefault())
}
func (m *HeatmapColorMode) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid heatmap color mode: must be a string, one of `palette` or `opacity`")
}
mode := HeatmapColorMode{valuer.NewString(v)}
switch mode {
case HeatmapColorModePalette, HeatmapColorModeOpacity:
*m = mode
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid heatmap color mode %q: must be `palette` or `opacity`", v)
}
}
type HeatmapPalette struct{ valuer.String }
var (
HeatmapPaletteIce = HeatmapPalette{valuer.NewString("ice")}
HeatmapPaletteMoss = HeatmapPalette{valuer.NewString("moss")}
HeatmapPaletteRust = HeatmapPalette{valuer.NewString("rust")}
HeatmapPaletteGraphite = HeatmapPalette{valuer.NewString("graphite")}
HeatmapPaletteEmber = HeatmapPalette{valuer.NewString("ember")}
HeatmapPaletteLagoon = HeatmapPalette{valuer.NewString("lagoon")}
HeatmapPaletteOrchid = HeatmapPalette{valuer.NewString("orchid")}
HeatmapPaletteVerdant = HeatmapPalette{valuer.NewString("verdant")}
HeatmapPaletteLava = HeatmapPalette{valuer.NewString("lava")} // default
HeatmapPaletteBeacon = HeatmapPalette{valuer.NewString("beacon")}
)
func (HeatmapPalette) Enum() []any {
return []any{
HeatmapPaletteIce, HeatmapPaletteMoss, HeatmapPaletteRust, HeatmapPaletteGraphite,
HeatmapPaletteEmber, HeatmapPaletteLagoon, HeatmapPaletteOrchid, HeatmapPaletteVerdant,
HeatmapPaletteLava, HeatmapPaletteBeacon,
}
}
func (p HeatmapPalette) ValueOrDefault() string {
if p.IsZero() {
return HeatmapPaletteLava.StringValue()
}
return p.StringValue()
}
func (p HeatmapPalette) MarshalJSON() ([]byte, error) {
return json.Marshal(p.ValueOrDefault())
}
func (p *HeatmapPalette) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid heatmap palette: must be a string, one of `ice`, `moss`, `rust`, `graphite`, `ember`, `lagoon`, `orchid`, `verdant`, `lava`, or `beacon`")
}
palette := HeatmapPalette{valuer.NewString(v)}
switch palette {
case HeatmapPaletteIce, HeatmapPaletteMoss, HeatmapPaletteRust, HeatmapPaletteGraphite,
HeatmapPaletteEmber, HeatmapPaletteLagoon, HeatmapPaletteOrchid, HeatmapPaletteVerdant,
HeatmapPaletteLava, HeatmapPaletteBeacon:
*p = palette
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid heatmap palette %q: must be `ice`, `moss`, `rust`, `graphite`, `ember`, `lagoon`, `orchid`, `verdant`, `lava`, or `beacon`", v)
}
}
type HeatmapYScale struct{ valuer.String }
var (
HeatmapYScaleAuto = HeatmapYScale{valuer.NewString("auto")} // default
HeatmapYScaleLinear = HeatmapYScale{valuer.NewString("linear")}
HeatmapYScaleLog = HeatmapYScale{valuer.NewString("log")}
HeatmapYScaleSymlog = HeatmapYScale{valuer.NewString("symlog")}
)
func (HeatmapYScale) Enum() []any {
return []any{HeatmapYScaleAuto, HeatmapYScaleLinear, HeatmapYScaleLog, HeatmapYScaleSymlog}
}
func (s HeatmapYScale) ValueOrDefault() string {
if s.IsZero() {
return HeatmapYScaleAuto.StringValue()
}
return s.StringValue()
}
func (s HeatmapYScale) MarshalJSON() ([]byte, error) {
return json.Marshal(s.ValueOrDefault())
}
func (s *HeatmapYScale) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid heatmap y scale: must be a string, one of `auto`, `linear`, `log`, or `symlog`")
}
scale := HeatmapYScale{valuer.NewString(v)}
switch scale {
case HeatmapYScaleAuto, HeatmapYScaleLinear, HeatmapYScaleLog, HeatmapYScaleSymlog:
*s = scale
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid heatmap y scale %q: must be `auto`, `linear`, `log`, or `symlog`", v)
}
}
type HeatmapColorScale struct{ valuer.String }
var (
HeatmapColorScaleLog = HeatmapColorScale{valuer.NewString("log")} // default
HeatmapColorScaleSqrt = HeatmapColorScale{valuer.NewString("sqrt")}
HeatmapColorScaleLinear = HeatmapColorScale{valuer.NewString("linear")}
)
func (HeatmapColorScale) Enum() []any {
return []any{HeatmapColorScaleLog, HeatmapColorScaleSqrt, HeatmapColorScaleLinear}
}
func (s HeatmapColorScale) ValueOrDefault() string {
if s.IsZero() {
return HeatmapColorScaleLog.StringValue()
}
return s.StringValue()
}
func (s HeatmapColorScale) MarshalJSON() ([]byte, error) {
return json.Marshal(s.ValueOrDefault())
}
func (s *HeatmapColorScale) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidInput, "invalid heatmap color scale: must be a string, one of `log`, `sqrt`, or `linear`")
}
scale := HeatmapColorScale{valuer.NewString(v)}
switch scale {
case HeatmapColorScaleLog, HeatmapColorScaleSqrt, HeatmapColorScaleLinear:
*s = scale
return nil
default:
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "invalid heatmap color scale %q: must be `log`, `sqrt`, or `linear`", v)
}
}

View File

@@ -1,149 +0,0 @@
{
"display": {
"name": "latency",
"description": "how request duration is distributed"
},
"variables": [],
"panels": {
"p1": {
"kind": "Panel",
"spec": {
"display": {
"name": "request duration",
"description": ""
},
"plugin": {
"kind": "signoz/HeatmapPanel",
"spec": {
"visualization": {
"timePreference": "global_time"
},
"formatting": {
"unit": "s",
"decimalPrecision": "3"
},
"axes": {
"yScale": "log"
},
"legend": {
"position": "right",
"mode": "table"
},
"chartAppearance": {
"colors": {
"mode": "palette",
"palette": "ember",
"scale": "sqrt",
"steps": 8,
"minCount": 0,
"maxCount": 500
}
}
}
},
"links": [],
"queries": [
{
"kind": "heatmap",
"spec": {
"plugin": {
"kind": "signoz/CompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"disabled": true,
"stepInterval": 60,
"aggregations": [
{
"metricName": "http.server.request.duration",
"timeAggregation": "increase",
"spaceAggregation": "sum"
}
],
"groupBy": [
{
"name": "service.name"
}
],
"bucketOptions": {
"kind": "log",
"spec": {
"scale": 4
}
}
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "metrics",
"disabled": true,
"stepInterval": 60,
"aggregations": [
{
"metricName": "http.server.request.count",
"timeAggregation": "increase",
"spaceAggregation": "sum"
}
],
"groupBy": [
{
"name": "service.name"
}
],
"bucketOptions": {
"kind": "linear",
"spec": {
"maxValue": 1000,
"numBuckets": 20
}
}
}
},
{
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "A / B",
"bucketOptions": {
"kind": "log",
"spec": {
"scale": 2
}
}
}
}
]
}
}
}
}
]
}
}
},
"links": [],
"layouts": [
{
"kind": "Grid",
"spec": {
"items": [
{
"x": 0,
"y": 0,
"width": 12,
"height": 8,
"content": {
"$ref": "#/spec/panels/p1"
}
}
]
}
}
]
}