mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-04 15:31:08 +00:00
Compare commits
2 Commits
v0.149.0
...
fix/panel-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e18463b0c | ||
|
|
2e419f9587 |
@@ -12,7 +12,7 @@ import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import { RelativeDurationSuggestionOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
@@ -24,6 +24,7 @@ import dayjs from 'dayjs';
|
||||
import { Clock, PenLine, TriangleAlert } from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
|
||||
import { readPanelTypeFromUrl } from 'utils/readPanelTypeFromUrl';
|
||||
import { TimeRangeValidationResult } from 'utils/timeUtils';
|
||||
|
||||
import CalendarContainer from './CalendarContainer';
|
||||
@@ -121,21 +122,14 @@ function CustomTimePickerPopoverContent({
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const url = new URLSearchParams(window.location.search);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const activeTimezoneOffset = timezone.offset;
|
||||
|
||||
let panelTypeFromURL = url.get(QueryParams.panelTypes);
|
||||
|
||||
try {
|
||||
panelTypeFromURL = JSON.parse(panelTypeFromURL as string);
|
||||
} catch {
|
||||
// fallback → leave as-is
|
||||
}
|
||||
const panelTypeFromURL = readPanelTypeFromUrl();
|
||||
|
||||
const isLogsListView =
|
||||
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
|
||||
panelTypeFromURL !== PANEL_TYPES.TABLE &&
|
||||
panelTypeFromURL !== PANEL_TYPES.TIME_SERIES; // we do not select list view in the url
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
||||
getDateRange(minTime, maxTime, timezone.value),
|
||||
|
||||
@@ -140,11 +140,6 @@ export default function AlertRules({
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const panelType = record.condition.compositeQuery.panelType;
|
||||
if (panelType) {
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
}
|
||||
|
||||
params.set(QueryParams.ruleId, record.id);
|
||||
|
||||
navigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('ListAlertRules — row click navigation', () => {
|
||||
const [url] = safeNavigateMock.mock.calls[0];
|
||||
expect(url).toContain('/alerts/overview?');
|
||||
expect(url).toContain('ruleId=rule-1');
|
||||
expect(url).toContain('panelTypes=graph');
|
||||
expect(url).not.toContain('panelTypes');
|
||||
expect(url).toContain('compositeQuery=');
|
||||
});
|
||||
|
||||
|
||||
@@ -36,11 +36,6 @@ export function useAlertRulesHandlers(
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const panelType = rule.condition.compositeQuery.panelType;
|
||||
if (panelType) {
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
}
|
||||
|
||||
params.set(QueryParams.ruleId, rule.id);
|
||||
|
||||
return `${ROUTES.ALERT_OVERVIEW}?${params.toString()}`;
|
||||
|
||||
@@ -141,6 +141,9 @@ describe('useCreateAlerts', () => {
|
||||
|
||||
const composite = getCompositeQueryFromLastOpen();
|
||||
expect(composite.queryType).toBe(EQueryType.PROM);
|
||||
|
||||
const [url] = (window.open as jest.Mock).mock.calls[0];
|
||||
expect(url).not.toContain('panelTypes');
|
||||
});
|
||||
|
||||
it('falls back to the mapped queryType when widget has no queryType', () => {
|
||||
|
||||
@@ -83,7 +83,6 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(updatedQuery)),
|
||||
);
|
||||
params.set(QueryParams.panelTypes, widget.panelTypes);
|
||||
params.set(QueryParams.version, ENTITY_VERSION_V5);
|
||||
params.set(QueryParams.source, YAxisSource.DASHBOARDS);
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { TestRouter } from 'tests/router';
|
||||
|
||||
import { useGetPanelTypesQueryParam } from './useGetPanelTypesQueryParam';
|
||||
|
||||
const renderWithSearch = (
|
||||
search: string,
|
||||
defaultPanelType?: PANEL_TYPES,
|
||||
): PANEL_TYPES | null => {
|
||||
const { result } = renderHook(
|
||||
() => useGetPanelTypesQueryParam(defaultPanelType),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestRouter initialRoute={`/logs/logs-explorer${search}`}>
|
||||
{children}
|
||||
</TestRouter>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
return result.current;
|
||||
};
|
||||
|
||||
describe('useGetPanelTypesQueryParam', () => {
|
||||
it.each([
|
||||
['a JSON encoded panel type', '?panelTypes=%22table%22', PANEL_TYPES.TABLE],
|
||||
['a plain string panel type', '?panelTypes=graph', PANEL_TYPES.TIME_SERIES],
|
||||
['an unparseable panel type', '?panelTypes=%7Bfoo', PANEL_TYPES.LIST],
|
||||
[
|
||||
'a value that is not a panel type',
|
||||
'?panelTypes=%22nope%22',
|
||||
PANEL_TYPES.LIST,
|
||||
],
|
||||
['an absent param', '', PANEL_TYPES.LIST],
|
||||
])('reads %s', (_, search, expected) => {
|
||||
expect(renderWithSearch(search, PANEL_TYPES.LIST)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { parsePanelType } from 'utils/parsePanelType';
|
||||
|
||||
export const useGetPanelTypesQueryParam = <T extends PANEL_TYPES | undefined>(
|
||||
defaultPanelType?: T,
|
||||
@@ -11,6 +12,10 @@ export const useGetPanelTypesQueryParam = <T extends PANEL_TYPES | undefined>(
|
||||
return useMemo(() => {
|
||||
const panelTypeQuery = urlQuery.get(QueryParams.panelTypes);
|
||||
|
||||
return panelTypeQuery ? JSON.parse(panelTypeQuery) : defaultPanelType;
|
||||
}, [urlQuery, defaultPanelType]);
|
||||
return (
|
||||
(panelTypeQuery ? parsePanelType(panelTypeQuery) : null) ?? defaultPanelType
|
||||
);
|
||||
}, [urlQuery, defaultPanelType]) as T extends undefined
|
||||
? PANEL_TYPES | null
|
||||
: PANEL_TYPES;
|
||||
};
|
||||
|
||||
@@ -184,7 +184,6 @@ describe('useCreateAlertFromPanel', () => {
|
||||
// The resolved query is seeded with the panel-derived alert prefill.
|
||||
expect(mockBuildAlertUrl).toHaveBeenCalledWith(
|
||||
{ resolved: 'query' },
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
undefined,
|
||||
mockPrefill,
|
||||
);
|
||||
|
||||
@@ -88,7 +88,6 @@ export function useCreateAlertFromPanel(): (
|
||||
const unit = readPanelUnit(panel.spec.plugin);
|
||||
const url = buildAlertUrl(
|
||||
query,
|
||||
panelType,
|
||||
unit,
|
||||
deriveAlertPrefill(panel, query, unit),
|
||||
);
|
||||
|
||||
@@ -65,14 +65,19 @@ describe('buildCreateAlertUrl', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('tags the URL with panel type, v5 version, and the dashboards source', () => {
|
||||
it('tags the URL with the v5 version and the dashboards source', () => {
|
||||
const params = parse(buildCreateAlertUrl(makePanel()));
|
||||
|
||||
expect(params.get(QueryParams.panelTypes)).toBe(PANEL_TYPES.TIME_SERIES);
|
||||
expect(params.get(QueryParams.version)).toBe(ENTITY_VERSION_V5);
|
||||
expect(params.get(QueryParams.source)).toBe('dashboards');
|
||||
});
|
||||
|
||||
it('does not tag the URL with a panel type, which the alert page ignores', () => {
|
||||
const params = parse(buildCreateAlertUrl(makePanel()));
|
||||
|
||||
expect(params.get(QueryParams.panelTypes)).toBeNull();
|
||||
});
|
||||
|
||||
it('encodes the translated query as the compositeQuery param', () => {
|
||||
const params = parse(buildCreateAlertUrl(makePanel()));
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import {
|
||||
@@ -34,7 +33,6 @@ export function readPanelUnit(
|
||||
*/
|
||||
export function buildAlertUrl(
|
||||
query: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
unit?: string,
|
||||
prefill?: PanelAlertPrefill,
|
||||
): string {
|
||||
@@ -48,7 +46,6 @@ export function buildAlertUrl(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
);
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
params.set(QueryParams.version, ENTITY_VERSION_V5);
|
||||
params.set(QueryParams.source, YAxisSource.DASHBOARDS);
|
||||
|
||||
@@ -76,10 +73,5 @@ export function buildCreateAlertUrl(panel: DashboardtypesPanelDTO): string {
|
||||
const panelType = toPanelType(panel.spec.plugin.kind);
|
||||
const query = fromPerses(panel.spec.queries, panelType);
|
||||
const unit = readPanelUnit(panel.spec.plugin);
|
||||
return buildAlertUrl(
|
||||
query,
|
||||
panelType,
|
||||
unit,
|
||||
deriveAlertPrefill(panel, query, unit),
|
||||
);
|
||||
return buildAlertUrl(query, unit, deriveAlertPrefill(panel, query, unit));
|
||||
}
|
||||
|
||||
@@ -118,12 +118,8 @@ export function QueryBuilderProvider({
|
||||
null,
|
||||
);
|
||||
|
||||
const panelTypeQueryParams = getUnstableCurrentSearchParams().get(
|
||||
QueryParams.panelTypes,
|
||||
) as PANEL_TYPES | null;
|
||||
|
||||
const [panelType, setPanelType] = useState<PANEL_TYPES | null>(
|
||||
panelTypeQueryParams,
|
||||
const [panelType, setPanelType] = useState<PANEL_TYPES | null>(() =>
|
||||
readPanelTypeFromUrl(),
|
||||
);
|
||||
|
||||
const [currentQuery, setCurrentQuery] = useState<QueryState>(queryState);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
|
||||
const renderWithPanelTypes = (value: string): PANEL_TYPES | null => {
|
||||
__setSearchParamsGetterForTest(
|
||||
() => new URLSearchParams({ panelTypes: value }),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useQueryBuilder(), {
|
||||
wrapper: AllTheProviders,
|
||||
});
|
||||
|
||||
return result.current.panelType;
|
||||
};
|
||||
|
||||
describe('QueryBuilderProvider panel type seed', () => {
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'a JSON encoded value',
|
||||
JSON.stringify(PANEL_TYPES.TABLE),
|
||||
PANEL_TYPES.TABLE,
|
||||
],
|
||||
['a plain string value', PANEL_TYPES.TABLE, PANEL_TYPES.TABLE],
|
||||
['a malformed value', '{foo', null],
|
||||
])('seeds from %s', (_, value, expected) => {
|
||||
expect(renderWithPanelTypes(value)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -30,11 +30,15 @@ describe('readPanelTypeFromUrl', () => {
|
||||
expect(readPanelTypeFromUrl()).toBe(PANEL_TYPES.LIST);
|
||||
});
|
||||
|
||||
it('returns the raw value instead of throwing on a malformed one', () => {
|
||||
withParam('{list');
|
||||
it.each(['{list', JSON.stringify('nope')])(
|
||||
'returns the fallback for %s, which is not a panel type',
|
||||
(value) => {
|
||||
withParam(value);
|
||||
|
||||
expect(readPanelTypeFromUrl()).toBe('{list');
|
||||
});
|
||||
expect(readPanelTypeFromUrl(PANEL_TYPES.LIST)).toBe(PANEL_TYPES.LIST);
|
||||
expect(readPanelTypeFromUrl()).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it('returns the fallback when the param is absent', () => {
|
||||
withParam();
|
||||
|
||||
18
frontend/src/utils/parsePanelType.ts
Normal file
18
frontend/src/utils/parsePanelType.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
const PANEL_TYPE_VALUES = new Set<string>(Object.values(PANEL_TYPES));
|
||||
|
||||
// The param is JSON encoded, but older alert links and bookmarks carry it as a plain string.
|
||||
export const parsePanelType = (value: string): PANEL_TYPES | null => {
|
||||
let parsed: unknown = value;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
parsed = value;
|
||||
}
|
||||
|
||||
return typeof parsed === 'string' && PANEL_TYPE_VALUES.has(parsed)
|
||||
? (parsed as PANEL_TYPES)
|
||||
: null;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import { getUnstableCurrentSearchParams } from './getUnstableCurrentSearchParams';
|
||||
import { parsePanelType } from './parsePanelType';
|
||||
|
||||
// The param is JSON-encoded, but a hand-written link can carry it unencoded.
|
||||
export const readPanelTypeFromUrl = (
|
||||
@@ -9,13 +10,5 @@ export const readPanelTypeFromUrl = (
|
||||
): PANEL_TYPES | null => {
|
||||
const raw = getUnstableCurrentSearchParams().get(QueryParams.panelTypes);
|
||||
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as PANEL_TYPES;
|
||||
} catch {
|
||||
return raw as PANEL_TYPES;
|
||||
}
|
||||
return (raw ? parsePanelType(raw) : null) ?? fallback;
|
||||
};
|
||||
|
||||
@@ -20,9 +20,6 @@ var (
|
||||
ErrCodeUnknownIdentifier = errors.MustNewCode("unknown_identifier")
|
||||
ErrCodeIllegalArgument = errors.MustNewCode("illegal_argument")
|
||||
|
||||
ErrCodeDistributedSubqueryDenied = errors.MustNewCode("distributed_subquery_denied")
|
||||
ErrCodeDecimalOverflow = errors.MustNewCode("decimal_overflow")
|
||||
|
||||
ErrCodeQueryCanceled = errors.MustNewCode("query_canceled")
|
||||
ErrCodeQueryTimeout = errors.MustNewCode("query_timeout")
|
||||
ErrCodeExecutionFailed = errors.MustNewCode("execution_failed")
|
||||
@@ -75,18 +72,6 @@ var clickHouseExceptionWrappers = map[chproto.Error]func(cause error, ex *clickh
|
||||
chproto.ErrTooLessArgumentsForFunction: func(cause error, ex *clickhouse.Exception) error {
|
||||
return errors.WrapInvalidInputf(cause, ErrCodeIllegalArgument, "too few arguments to function: %s", ex.Message)
|
||||
},
|
||||
chproto.ErrBadGet: func(cause error, ex *clickhouse.Exception) error {
|
||||
return errors.WrapInvalidInputf(cause, ErrCodeIllegalArgument, "parameter of the wrong type: %s", ex.Message).
|
||||
WithSuggestions("write numeric function parameters in the type the function expects, for example 1.0 rather than 1 where it takes a Float64")
|
||||
},
|
||||
chproto.ErrDistributedInJoinSubqueryDenied: func(cause error, ex *clickhouse.Exception) error {
|
||||
return errors.WrapInvalidInputf(cause, ErrCodeDistributedSubqueryDenied, "IN or JOIN subquery over a distributed table is not allowed: %s", ex.Message).
|
||||
WithSuggestions("use GLOBAL IN or GLOBAL JOIN for the subquery")
|
||||
},
|
||||
chproto.ErrDecimalOverflow: func(cause error, ex *clickhouse.Exception) error {
|
||||
return errors.WrapInvalidInputf(cause, ErrCodeDecimalOverflow, "decimal overflow: %s", ex.Message).
|
||||
WithSuggestions("cast the Decimal values to Float64, or to a wider Decimal such as Decimal128, before the arithmetic")
|
||||
},
|
||||
}
|
||||
|
||||
type provider struct {
|
||||
|
||||
Reference in New Issue
Block a user