Compare commits

...

9 Commits

Author SHA1 Message Date
Yunus M
ec5b5424c6 chore: update test cases 2026-02-25 12:59:33 +05:30
Yunus M
d30abb762b chore: support cmd click on all clickable items 2026-02-25 12:59:33 +05:30
Ishan
c579614d56 feat: color fallback and red checks (#10389)
* feat: color fallback and red checks

* feat: testcase added
2026-02-25 11:54:22 +05:30
Ishan
78ba2ba356 feat: text selection block (#10373)
* feat: text selection block

* chore: added test file
2026-02-25 11:38:15 +05:30
Ishan
7fd4762e2a feat: ui bugs body width and table css (#10377)
* feat: ui bugs body width and table css

* feat: defualt open search overview

* feat: added timerRef to cleanup
2026-02-25 11:25:54 +05:30
Nageshbansal
4e4c9ce5af chore: enable metadataexporter in docker (#10409)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-25 03:13:27 +05:30
Srikanth Chekuri
7605775a38 chore: remove support for non v5 version in rules (#10406) 2026-02-24 23:16:21 +05:30
Vinicius Lourenço
cb1a2a8a13 perf(bundle-size): lazy load pages to reduce main bundle size (#10230)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-24 10:41:40 +00:00
Nikhil Soni
1a5d37b25a fix: add missing filtering for ip address for scalar data (#10264)
* fix: add missing filtering for ip address for scalar data

In domain listing api for external api monitoring,
we have option to filter out the IP address but
it only handles timeseries and raw type data while
domain list handler returns scalar data.

* fix: switch to new derived attributes for ip filtering

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2026-02-24 10:26:10 +00:00
72 changed files with 1500 additions and 337 deletions

View File

@@ -82,6 +82,12 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -93,19 +99,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, signozmeter]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, signozmeter]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -82,6 +82,12 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -93,19 +99,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, signozmeter]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, signozmeter]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -2,6 +2,8 @@
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
event?: MouseEvent | React.MouseEvent;
}
interface SafeNavigateTo {
@@ -20,9 +22,7 @@ interface UseSafeNavigateReturn {
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: jest.fn(
(to: SafeNavigateToType, options?: SafeNavigateOptions) => {
console.log(`Mock safeNavigate called with:`, to, options);
},
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
) as jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,

View File

@@ -308,3 +308,15 @@ export const PublicDashboardPage = Loadable(
/* webpackChunkName: "Public Dashboard Page" */ 'pages/PublicDashboard'
),
);
export const AlertTypeSelectionPage = Loadable(
() =>
import(
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
),
);
export const MeterExplorerPage = Loadable(
() =>
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
);

View File

@@ -1,12 +1,10 @@
import { RouteProps } from 'react-router-dom';
import ROUTES from 'constants/routes';
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import {
AlertHistory,
AlertOverview,
AlertTypeSelectionPage,
AllAlertChannels,
AllErrors,
ApiMonitoring,
@@ -29,6 +27,8 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
Onboarding,
@@ -399,28 +399,28 @@ const routes: AppRoutes[] = [
{
path: ROUTES.MESSAGING_QUEUES_KAFKA,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_KAFKA',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_CELERY_TASK',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_OVERVIEW,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_KAFKA_DETAIL',
isPrivate: true,
},
@@ -463,21 +463,21 @@ const routes: AppRoutes[] = [
{
path: ROUTES.METER,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
@@ -214,7 +214,7 @@ function LogDetailInner({
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (): void => {
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
@@ -227,7 +227,9 @@ function LogDetailInner({
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
event: e,
});
};
const handleQueryExpressionChange = useCallback(

View File

@@ -21,7 +21,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '60rem',
maxWidth: '90rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@@ -70,8 +70,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
</Tag>
) : undefined
}
onClick={(): void => {
onSelect(option.selection);
onClick={(e): void => {
onSelect(option.selection, e);
}}
data-testid={`alert-type-card-${option.selection}`}
>
@@ -108,7 +108,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
}
interface SelectAlertTypeProps {
onSelect: (typ: AlertTypes) => void;
onSelect: (type: AlertTypes, event?: React.MouseEvent) => void;
}
export default SelectAlertType;

View File

@@ -33,9 +33,9 @@ function Footer(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const handleDiscard = (): void => {
const handleDiscard = (e: React.MouseEvent): void => {
discardAlertRule();
safeNavigate('/alerts');
safeNavigate('/alerts', { event: e });
};
const alertValidationMessage = useMemo(

View File

@@ -161,6 +161,7 @@ describe('Dashboard landing page actions header tests', () => {
// Verify navigation was called with correct URL
expect(mockSafeNavigate).toHaveBeenCalledWith(
'/dashboard?columnKey=updatedAt&order=descend&page=1&search=',
expect.objectContaining({ event: expect.any(Object) }),
);
// Ensure the URL contains only essential dashboard list params

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import { Button } from 'antd';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -23,16 +23,19 @@ function DashboardBreadcrumbs(): JSX.Element {
const { title = '', image = Base64Icons[0] } = selectedData || {};
const goToListPage = useCallback(() => {
const urlParams = new URLSearchParams();
urlParams.set('columnKey', listSortOrder.columnKey as string);
urlParams.set('order', listSortOrder.order as string);
urlParams.set('page', listSortOrder.pagination as string);
urlParams.set('search', listSortOrder.search as string);
const goToListPage = useCallback(
(e?: React.MouseEvent) => {
const urlParams = new URLSearchParams();
urlParams.set('columnKey', listSortOrder.columnKey as string);
urlParams.set('order', listSortOrder.order as string);
urlParams.set('page', listSortOrder.pagination as string);
urlParams.set('search', listSortOrder.search as string);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
safeNavigate(generatedUrl);
}, [listSortOrder, safeNavigate]);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
safeNavigate(generatedUrl, { event: e });
},
[listSortOrder, safeNavigate],
);
return (
<div className="dashboard-breadcrumbs">

View File

@@ -16,6 +16,7 @@ import { isUndefined } from 'lodash-es';
import { urlKey } from 'pages/ErrorDetails/utils';
import { useTimezone } from 'providers/Timezone';
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { keyToExclude } from './config';
import { DashedContainer, EditorContainer, EventContainer } from './styles';
@@ -111,14 +112,19 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
}));
const onClickTraceHandler = (): void => {
const onClickTraceHandler = (event: React.MouseEvent): void => {
logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
const path = `/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`;
if (isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
history.push(path);
}
};
const logEventCalledRef = useRef(false);

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
@@ -329,13 +329,18 @@ function FormAlertRules({
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
const onCancelHandler = useCallback(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [safeNavigate, urlQuery]);
const onCancelHandler = useCallback(
(e?: React.MouseEvent) => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
event: e,
});
},
[safeNavigate, urlQuery],
);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults

View File

@@ -1,5 +1,11 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import {
LoadingOutlined,
@@ -279,9 +285,9 @@ function FullView({
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
if (dashboardEditView) {
safeNavigate(dashboardEditView);
safeNavigate(dashboardEditView, { event: e });
}
}}
>

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover } from 'antd';
@@ -28,6 +28,7 @@ import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { isIngestionActive } from 'utils/app';
import { navigateToPage } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import AlertRules from './AlertRules/AlertRules';
@@ -413,12 +414,12 @@ export default function Home(): JSX.Element {
role="button"
tabIndex={0}
className="active-ingestion-card-actions"
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
// eslint-disable-next-line sonarjs/no-duplicate-string
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -455,11 +456,11 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -496,11 +497,11 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER);
navigateToPage(ROUTES.METRICS_EXPLORER, history.push, e);
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -550,11 +551,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
}}
>
Open Logs Explorer
@@ -564,11 +565,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
}}
>
Open Traces Explorer
@@ -578,11 +579,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
navigateToPage(ROUTES.METRICS_EXPLORER_EXPLORER, history.push, e);
}}
>
Open Metrics Explorer
@@ -619,11 +620,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
});
history.push(ROUTES.ALL_DASHBOARD);
navigateToPage(ROUTES.ALL_DASHBOARD, history.push, e);
}}
>
Create dashboard
@@ -661,11 +662,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Alerts',
});
history.push(ROUTES.ALERTS_NEW);
navigateToPage(ROUTES.ALERTS_NEW, history.push, e);
}}
>
Create an alert

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { QueryKey } from 'react-query';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
@@ -116,7 +116,7 @@ const ServicesListTable = memo(
onRowClick,
}: {
services: ServicesList[];
onRowClick: (record: ServicesList) => void;
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
}): JSX.Element => (
<div className="services-list-container home-data-item-container metrics-services-list">
<div className="services-list">
@@ -125,8 +125,8 @@ const ServicesListTable = memo(
dataSource={services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => onRowClick(record),
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
})}
/>
</div>
@@ -284,11 +284,11 @@ function ServiceMetrics({
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
const handleRowClick = useCallback(
(record: ServicesList) => {
(record: ServicesList, event: React.MouseEvent) => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
},
[safeNavigate],
);

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { Button, Select, Skeleton, Table } from 'antd';
@@ -172,13 +172,13 @@ export default function ServiceTraces({
dataSource={top5Services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => {
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
},
})}
/>

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import {
Skeleton,
@@ -11,6 +11,7 @@ import {
import { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { InfraMonitoringEvents } from 'constants/events';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
import {
@@ -76,7 +77,16 @@ export default function HostsListTable({
[],
);
const handleRowClick = (record: HostRowData): void => {
const handleRowClick = (
record: HostRowData,
event: React.MouseEvent,
): void => {
if (isModifierKeyPressed(event)) {
const params = new URLSearchParams(window.location.search);
params.set('hostName', record.hostName);
openInNewTab(`${window.location.pathname}?${params.toString()}`);
return;
}
onHostClick(record.hostName);
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.HostEntity,
@@ -182,8 +192,8 @@ export default function HostsListTable({
(record as HostRowData & { key: string }).key ?? record.hostName
}
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record: HostRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
/>

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -24,6 +24,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -451,7 +452,19 @@ function K8sClustersList({
);
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
const handleRowClick = (record: K8sClustersRowData): void => {
const handleRowClick = (
record: K8sClustersRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
record.clusterUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedClusterName(record.clusterUID);
@@ -515,8 +528,19 @@ function K8sClustersList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
record.clusterUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedClusterName(record.clusterUID);
},
className: 'expanded-clickable-row',
@@ -707,8 +731,10 @@ function K8sClustersList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -457,7 +458,19 @@ function K8sDaemonSetsList({
nestedDaemonSetsData,
]);
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
const handleRowClick = (
record: K8sDaemonSetsRowData,
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
record.daemonsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedDaemonSetUID(record.daemonsetUID);
@@ -521,8 +534,19 @@ function K8sDaemonSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
record.daemonsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedDaemonSetUID(record.daemonsetUID);
},
className: 'expanded-clickable-row',
@@ -715,8 +739,10 @@ function K8sDaemonSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -463,7 +464,19 @@ function K8sDeploymentsList({
nestedDeploymentsData,
]);
const handleRowClick = (record: K8sDeploymentsRowData): void => {
const handleRowClick = (
record: K8sDeploymentsRowData,
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
record.deploymentUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedDeploymentUID(record.deploymentUID);
@@ -527,8 +540,19 @@ function K8sDeploymentsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
record.deploymentUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedDeploymentUID(record.deploymentUID);
},
className: 'expanded-clickable-row',
@@ -722,8 +746,10 @@ function K8sDeploymentsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -428,7 +429,16 @@ function K8sJobsList({
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
const handleRowClick = (record: K8sJobsRowData): void => {
const handleRowClick = (
record: K8sJobsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedJobUID(record.jobUID);
@@ -492,8 +502,16 @@ function K8sJobsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedJobUID(record.jobUID);
},
className: 'expanded-clickable-row',
@@ -684,8 +702,10 @@ function K8sJobsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -24,6 +24,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -459,7 +460,19 @@ function K8sNamespacesList({
nestedNamespacesData,
]);
const handleRowClick = (record: K8sNamespacesRowData): void => {
const handleRowClick = (
record: K8sNamespacesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
record.namespaceUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
@@ -523,8 +536,19 @@ function K8sNamespacesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
record.namespaceUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedNamespaceUID(record.namespaceUID);
},
className: 'expanded-clickable-row',
@@ -716,8 +740,10 @@ function K8sNamespacesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -24,6 +24,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -438,7 +439,16 @@ function K8sNodesList({
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
const handleRowClick = (record: K8sNodesRowData): void => {
const handleRowClick = (
record: K8sNodesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedNodeUID(record.nodeUID);
@@ -503,8 +513,19 @@ function K8sNodesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
record.nodeUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedNodeUID(record.nodeUID);
},
className: 'expanded-clickable-row',
@@ -695,8 +716,10 @@ function K8sNodesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -26,6 +26,7 @@ import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -495,7 +496,16 @@ function K8sPodsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const handleRowClick = (record: K8sPodsRowData): void => {
const handleRowClick = (
record: K8sPodsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedPodUID(record.podUID);
setSearchParams({
@@ -615,8 +625,14 @@ function K8sPodsList({
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedPodUID(record.podUID);
},
className: 'expanded-clickable-row',
@@ -752,8 +768,8 @@ function K8sPodsList({
scroll={{ x: true }}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -460,7 +461,19 @@ function K8sStatefulSetsList({
nestedStatefulSetsData,
]);
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
const handleRowClick = (
record: K8sStatefulSetsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
record.statefulsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedStatefulSetUID(record.statefulsetUID);
@@ -524,8 +537,19 @@ function K8sStatefulSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
record.statefulsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedStatefulSetUID(record.statefulsetUID);
},
className: 'expanded-clickable-row',
@@ -718,8 +742,8 @@ function K8sStatefulSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record) => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -390,7 +391,16 @@ function K8sVolumesList({
);
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
const handleRowClick = (record: K8sVolumesRowData): void => {
const handleRowClick = (
record: K8sVolumesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedVolumeUID(record.volumeUID);
@@ -454,8 +464,19 @@ function K8sVolumesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
record.volumeUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedVolumeUID(record.volumeUID);
},
className: 'expanded-clickable-row',
@@ -641,8 +662,10 @@ function K8sVolumesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -7,6 +7,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
@@ -36,9 +37,13 @@ export function AlertsEmptyState(): JSX.Element {
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback(() => {
const onClickNewAlertHandler = useCallback((e: React.MouseEvent) => {
setLoading(false);
history.push(ROUTES.ALERTS_NEW);
if (isModifierKeyPressed(e)) {
openInNewTab(ROUTES.ALERTS_NEW);
} else {
history.push(ROUTES.ALERTS_NEW);
}
}, []);
return (

View File

@@ -1,5 +1,5 @@
/* eslint-disable react/display-name */
import { useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { PlusOutlined } from '@ant-design/icons';
@@ -100,16 +100,22 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
});
}, [notificationsApi, t]);
const onClickNewAlertHandler = useCallback(() => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
const onClickNewAlertHandler = useCallback(
(e: React.MouseEvent): void => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, { event: e });
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
[],
);
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
const onEditHandler = (
record: GettableAlert,
options?: { event?: React.MouseEvent; newTab?: boolean },
): void => {
const compositeQuery = sanitizeDefaultAlertQuery(
mapQueryDataFromApi(record.condition.compositeQuery),
record.alertType as AlertTypes,
@@ -125,11 +131,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setEditLoader(false);
if (openInNewTab) {
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
} else {
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, {
event: options?.event,
newTab: options?.newTab,
});
};
const onCloneHandler = (
@@ -266,7 +271,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
e.stopPropagation();
e.preventDefault();
onEditHandler(record, e.metaKey || e.ctrlKey);
onEditHandler(record, { event: e });
};
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
@@ -331,7 +336,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
/>,
<ColumnButton
key="2"
onClick={(): void => onEditHandler(record, false)}
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { event: e })
}
type="link"
loading={editLoader}
>
@@ -339,7 +346,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</ColumnButton>,
<ColumnButton
key="3"
onClick={(): void => onEditHandler(record, true)}
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>

View File

@@ -416,11 +416,7 @@ function DashboardsList(): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}
safeNavigate(getLink(), { event });
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
dashboardName: dashboard.name,

View File

@@ -1,6 +1,7 @@
import { ReactNode, useState } from 'react';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import type { InputRef } from 'antd';
import {
Button,
Collapse,
@@ -46,12 +47,23 @@ function Overview({
handleChangeSelectedView,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
true,
);
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const focusTimerRef = useRef<ReturnType<typeof setTimeout>>();
const searchInputRef = useCallback((node: InputRef | null) => {
clearTimeout(focusTimerRef.current);
if (node) {
focusTimerRef.current = setTimeout(() => node.focus(), 100);
}
}, []);
useEffect(() => (): void => clearTimeout(focusTimerRef.current), []);
const isDarkMode = useIsDarkMode();
const options: EditorProps['options'] = {
@@ -196,7 +208,7 @@ function Overview({
<>
{isSearchVisible && (
<Input
autoFocus
ref={searchInputRef}
placeholder="Search for a field..."
className="search-input"
value={fieldSearchInput}

View File

@@ -0,0 +1,34 @@
import { Color } from '@signozhq/design-tokens';
import { getColorsForSeverityLabels, isRedLike } from '../utils';
describe('getColorsForSeverityLabels', () => {
it('should return slate for blank labels', () => {
expect(getColorsForSeverityLabels('', 0)).toBe(Color.BG_SLATE_300);
expect(getColorsForSeverityLabels(' ', 0)).toBe(Color.BG_SLATE_300);
});
it('should return correct colors for known severity variants', () => {
expect(getColorsForSeverityLabels('INFO', 0)).toBe(Color.BG_ROBIN_600);
expect(getColorsForSeverityLabels('ERROR', 0)).toBe(Color.BG_CHERRY_600);
expect(getColorsForSeverityLabels('WARN', 0)).toBe(Color.BG_AMBER_600);
expect(getColorsForSeverityLabels('DEBUG', 0)).toBe(Color.BG_AQUA_600);
expect(getColorsForSeverityLabels('TRACE', 0)).toBe(Color.BG_FOREST_600);
expect(getColorsForSeverityLabels('FATAL', 0)).toBe(Color.BG_SAKURA_600);
});
it('should return non-red colors for unrecognized labels at any index', () => {
for (let i = 0; i < 30; i++) {
const color = getColorsForSeverityLabels('4', i);
expect(isRedLike(color)).toBe(false);
}
});
it('should return non-red colors for numeric severity text', () => {
const numericLabels = ['1', '2', '4', '9', '13', '17', '21'];
numericLabels.forEach((label) => {
const color = getColorsForSeverityLabels(label, 0);
expect(isRedLike(color)).toBe(false);
});
});
});

View File

@@ -1,7 +1,16 @@
import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme';
import { colors } from 'lib/getRandomColor';
// Function to determine if a color is "red-like" based on its RGB values
export function isRedLike(hex: string): boolean {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return r > 180 && r > g * 1.4 && r > b * 1.4;
}
const SAFE_FALLBACK_COLORS = colors.filter((c) => !isRedLike(c));
const SEVERITY_VARIANT_COLORS: Record<string, string> = {
TRACE: Color.BG_FOREST_600,
Trace: Color.BG_FOREST_500,
@@ -67,8 +76,13 @@ export function getColorsForSeverityLabels(
label: string,
index: number,
): string {
// Check if we have a direct mapping for this severity variant
const variantColor = SEVERITY_VARIANT_COLORS[label.trim()];
const trimmed = label.trim();
if (!trimmed) {
return Color.BG_SLATE_300;
}
const variantColor = SEVERITY_VARIANT_COLORS[trimmed];
if (variantColor) {
return variantColor;
}
@@ -103,5 +117,8 @@ export function getColorsForSeverityLabels(
return Color.BG_SAKURA_500;
}
return colors[index % colors.length] || themeColors.red;
return (
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
Color.BG_SLATE_400
);
}

View File

@@ -111,23 +111,19 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => {
return (
<div key={log.id as string}>
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
</div>
);
},
(index: number, log: Record<string, unknown>): JSX.Element => (
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
),
[
tableColumns,
onSetActiveLog,
@@ -143,7 +139,8 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
{tableColumns
.filter((column) => column.key)
.map((column) => {
const isDragColumn = column.key !== 'expand';
const isDragColumn =
column.key !== 'expand' && column.key !== 'state-indicator';
return (
<TableHeaderCellStyled

View File

@@ -31,6 +31,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -235,7 +236,7 @@ function Application(): JSX.Element {
timestamp: number,
apmToTraceQuery: Query,
isViewLogsClicked?: boolean,
): (() => void) => (): void => {
): ((e: React.MouseEvent) => void) => (e: React.MouseEvent): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - stepInterval);
@@ -259,7 +260,11 @@ function Application(): JSX.Element {
queryString,
);
history.push(newPath);
if (isModifierKeyPressed(e)) {
openInNewTab(newPath);
} else {
history.push(newPath);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[stepInterval],

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
@@ -6,9 +7,9 @@ import './GraphControlsPanel.styles.scss';
interface GraphControlsPanelProps {
id: string;
onViewLogsClick?: () => void;
onViewTracesClick: () => void;
onViewAPIMonitoringClick?: () => void;
onViewLogsClick?: (e: React.MouseEvent) => void;
onViewTracesClick: (e: React.MouseEvent) => void;
onViewAPIMonitoringClick?: (e: React.MouseEvent) => void;
}
function GraphControlsPanel({

View File

@@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
import React, { Dispatch, SetStateAction, useMemo, useRef } from 'react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
@@ -42,7 +42,10 @@ interface OnViewTracePopupClickProps {
apmToTraceQuery: Query;
isViewLogsClicked?: boolean;
stepInterval?: number;
safeNavigate: (url: string) => void;
safeNavigate: (
url: string,
options?: { event?: React.MouseEvent | MouseEvent },
) => void;
}
interface OnViewAPIMonitoringPopupClickProps {
@@ -51,8 +54,10 @@ interface OnViewAPIMonitoringPopupClickProps {
stepInterval?: number;
domainName: string;
isError: boolean;
safeNavigate: (url: string) => void;
safeNavigate: (
url: string,
options?: { event?: React.MouseEvent | MouseEvent },
) => void;
}
export function generateExplorerPath(
@@ -93,8 +98,8 @@ export function onViewTracePopupClick({
isViewLogsClicked,
stepInterval,
safeNavigate,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
}: OnViewTracePopupClickProps): (e?: React.MouseEvent) => void {
return (e?: React.MouseEvent): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
@@ -118,7 +123,7 @@ export function onViewTracePopupClick({
queryString,
);
safeNavigate(newPath);
safeNavigate(newPath, { event: e });
};
}
@@ -149,8 +154,8 @@ export function onViewAPIMonitoringPopupClick({
isError,
stepInterval,
safeNavigate,
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
return (): void => {
}: OnViewAPIMonitoringPopupClickProps): (e?: React.MouseEvent) => void {
return (e?: React.MouseEvent): void => {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
@@ -190,7 +195,7 @@ export function onViewAPIMonitoringPopupClick({
filters,
);
safeNavigate(newPath);
safeNavigate(newPath, { event: e });
};
}

View File

@@ -8,6 +8,7 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Bell, Grid } from 'lucide-react';
import { navigateToPage } from 'utils/navigation';
import { DashboardsAndAlertsPopoverProps } from './types';
@@ -25,9 +26,10 @@ function DashboardsAndAlertsPopover({
label: (
<Typography.Link
key={alert.alert_id}
onClick={(): void => {
onClick={(e): void => {
params.set(QueryParams.ruleId, alert.alert_id);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
const path = `${ROUTES.ALERT_OVERVIEW}?${params.toString()}`;
navigateToPage(path, history.push, e);
}}
className="dashboards-popover-content-item"
>
@@ -55,11 +57,12 @@ function DashboardsAndAlertsPopover({
label: (
<Typography.Link
key={dashboard.dashboard_id}
onClick={(): void => {
onClick={(e): void => {
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboard_id,
}),
{ event: e },
);
}}
className="dashboards-popover-content-item"

View File

@@ -118,6 +118,7 @@ describe('DashboardsAndAlertsPopover', () => {
// Should navigate to the dashboard
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/dashboard/${mockDashboard1.dashboard_id}`,
expect.objectContaining({ event: expect.any(Object) }),
);
});

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import {
Spin,
@@ -107,8 +107,9 @@ function MetricsTable({
onChange: onPaginationChange,
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key, 'list'),
onRow={(record: MetricsListItemRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void =>
openMetricDetails(record.key, 'list', event),
className: 'clickable-row',
})}
/>

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
@@ -14,6 +14,7 @@ import { AppState } from 'store/reducers';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import InspectModal from '../Inspect';
@@ -209,7 +210,15 @@ function Summary(): JSX.Element {
const openMetricDetails = (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(IS_METRIC_DETAILS_OPEN_KEY, 'true');
newParams.set(SELECTED_METRIC_NAME_KEY, metricName);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedMetricName(metricName);
setIsMetricDetailsOpen(true);
setSearchParams({

View File

@@ -194,7 +194,11 @@ describe('MetricsTable', () => {
);
fireEvent.click(screen.getByText('Metric 1'));
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
expect(mockOpenMetricDetails).toHaveBeenCalledWith(
'metric1',
'list',
expect.any(Object),
);
});
it('calls setOrderBy when column header is clicked', () => {

View File

@@ -14,7 +14,11 @@ export interface MetricsTableProps {
onPaginationChange: (page: number, pageSize: number) => void;
setOrderBy: (orderBy: OrderByPayload) => void;
totalCount: number;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
queryFilters: TagFilter;
}
@@ -28,7 +32,11 @@ export interface MetricsTreemapProps {
isLoading: boolean;
isError: boolean;
viewType: TreemapViewType;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
setHeatmapView: (value: TreemapViewType) => void;
}

View File

@@ -1,9 +1,10 @@
import { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Badge, Button } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { Undo } from 'lucide-react';
import { navigateToPage } from 'utils/navigation';
import { buttonText, RIBBON_STYLES } from './config';
@@ -21,23 +22,30 @@ function NewExplorerCTA(): JSX.Element | null {
[location.pathname],
);
const onClickHandler = useCallback((): void => {
if (location.pathname === ROUTES.LOGS_EXPLORER) {
history.push(ROUTES.OLD_LOGS_EXPLORER);
} else if (location.pathname === ROUTES.TRACE) {
history.push(ROUTES.TRACES_EXPLORER);
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
history.push(ROUTES.LOGS_EXPLORER);
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
history.push(ROUTES.TRACE);
}
}, [location.pathname]);
const onClickHandler = useCallback(
(e?: React.MouseEvent): void => {
let targetPath: string;
if (location.pathname === ROUTES.LOGS_EXPLORER) {
targetPath = ROUTES.OLD_LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACE) {
targetPath = ROUTES.TRACES_EXPLORER;
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
targetPath = ROUTES.LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
targetPath = ROUTES.TRACE;
} else {
return;
}
navigateToPage(targetPath, history.push, e);
},
[location.pathname],
);
const button = useMemo(
() => (
<Button
icon={<Undo size={16} />}
onClick={onClickHandler}
onClick={(e): void => onClickHandler(e)}
data-testid="newExplorerCTA"
type="text"
className="periscope-btn link"

View File

@@ -1,6 +1,6 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useEffectOnce } from 'react-use';
@@ -17,6 +17,7 @@ import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingIn
import history from 'lib/history';
import { UserPlus } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { navigateToPage } from 'utils/navigation';
import ModuleStepsContainer from './common/ModuleStepsContainer/ModuleStepsContainer';
import { stepsMap } from './constants/stepsConfig';
@@ -252,9 +253,13 @@ export default function Onboarding(): JSX.Element {
}
};
const handleNext = (): void => {
const handleNext = (e?: React.MouseEvent): void => {
if (activeStep <= 3) {
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
navigateToPage(
moduleRouteMap[selectedModule.id as ModulesMap],
history.push,
e,
);
}
};
@@ -317,9 +322,9 @@ export default function Onboarding(): JSX.Element {
{activeStep === 1 && (
<div className="onboarding-page">
<div
onClick={(): void => {
onClick={(e): void => {
logEvent('Onboarding V2: Skip Button Clicked', {});
history.push(ROUTES.APPLICATION);
navigateToPage(ROUTES.APPLICATION, history.push, e);
}}
className="skip-to-console"
>
@@ -355,7 +360,11 @@ export default function Onboarding(): JSX.Element {
</div>
</div>
<div className="continue-to-next-step">
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
<Button
type="primary"
icon={<ArrowRightOutlined />}
onClick={(e): void => handleNext(e)}
>
{t('get_started')}
</Button>
</div>
@@ -386,17 +395,16 @@ export default function Onboarding(): JSX.Element {
{activeStep > 1 && (
<div className="stepsContainer">
<ModuleStepsContainer
onReselectModule={(): void => {
onReselectModule={(e?: React.MouseEvent): void => {
setCurrent(current - 1);
setActiveStep(activeStep - 1);
setSelectedModule(useCases.APM);
resetProgress();
if (isOnboardingV3Enabled) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
history.push(ROUTES.GET_STARTED);
}
const path = isOnboardingV3Enabled
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
navigateToPage(path, history.push, e);
}}
selectedModule={selectedModule}
selectedModuleSteps={selectedModuleSteps}

View File

@@ -1,6 +1,6 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { LoadingOutlined } from '@ant-design/icons';
@@ -23,6 +23,7 @@ import {
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react';
import { navigateToPage } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import './DataSource.styles.scss';
@@ -141,13 +142,13 @@ export default function DataSource(): JSX.Element {
}
};
const goToIntegrationsPage = (): void => {
const goToIntegrationsPage = (e?: React.MouseEvent): void => {
logEvent('Onboarding V2: Go to integrations', {
module: selectedModule?.id,
dataSource: selectedDataSource?.name,
framework: selectedFramework,
});
history.push(ROUTES.INTEGRATIONS);
navigateToPage(ROUTES.INTEGRATIONS, history.push, e);
};
return (
@@ -249,7 +250,7 @@ export default function DataSource(): JSX.Element {
page which allows more sources of sending data
</Typography.Text>
<Button
onClick={goToIntegrationsPage}
onClick={(e): void => goToIntegrationsPage(e)}
icon={<Blocks size={14} />}
className="navigate-integrations-page-btn"
>

View File

@@ -2,7 +2,7 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable react/jsx-no-comment-textnodes */
/* eslint-disable sonarjs/prefer-single-boolean-return */
import { SetStateAction, useState } from 'react';
import React, { SetStateAction, useState } from 'react';
import {
ArrowLeftOutlined,
ArrowRightOutlined,
@@ -19,6 +19,7 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es';
import { UserPlus } from 'lucide-react';
import { navigateToPage } from 'utils/navigation';
import { useOnboardingContext } from '../../context/OnboardingContext';
import {
@@ -142,7 +143,7 @@ export default function ModuleStepsContainer({
return true;
};
const redirectToModules = (): void => {
const redirectToModules = (event?: React.MouseEvent): void => {
logEvent('Onboarding V2 Complete', {
module: selectedModule.id,
dataSource: selectedDataSource?.id,
@@ -152,26 +153,28 @@ export default function ModuleStepsContainer({
serviceName,
});
let targetPath: string;
if (selectedModule.id === ModulesMap.APM) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else if (selectedModule.id === ModulesMap.LogsManagement) {
history.push(ROUTES.LOGS_EXPLORER);
targetPath = ROUTES.LOGS_EXPLORER;
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else if (selectedModule.id === ModulesMap.AwsMonitoring) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
}
navigateToPage(targetPath, history.push, event);
};
const handleNext = (): void => {
const handleNext = (event?: React.MouseEvent): void => {
const isValid = isValidForm();
if (isValid) {
if (current === lastStepIndex) {
resetProgress();
redirectToModules();
redirectToModules(event);
return;
}
@@ -379,8 +382,8 @@ export default function ModuleStepsContainer({
}
};
const handleLogoClick = (): void => {
history.push('/home');
const handleLogoClick = (e: React.MouseEvent): void => {
navigateToPage('/home', history.push, e);
};
return (
@@ -400,7 +403,7 @@ export default function ModuleStepsContainer({
style={{ display: 'flex', alignItems: 'center' }}
type="default"
icon={<LeftCircleOutlined />}
onClick={onReselectModule}
onClick={(e): void => onReselectModule(e)}
>
{selectedModule.title}
</Button>
@@ -470,7 +473,11 @@ export default function ModuleStepsContainer({
>
Back
</Button>
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
<Button
onClick={(e): void => handleNext(e)}
type="primary"
icon={<ArrowRightOutlined />}
>
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
</Button>
<LaunchChatSupport

View File

@@ -21,6 +21,7 @@ import history from 'lib/history';
import { isEmpty } from 'lodash-es';
import { CheckIcon, Goal, UserPlus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { navigateToPage } from 'utils/navigation';
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
import InviteTeamMembers from '../InviteTeamMembers/InviteTeamMembers';
@@ -413,7 +414,10 @@ function OnboardingAddDataSource(): JSX.Element {
]);
}, [org]);
const handleUpdateCurrentStep = (step: number): void => {
const handleUpdateCurrentStep = (
step: number,
event?: React.MouseEvent,
): void => {
setCurrentStep(step);
if (step === 1) {
@@ -443,43 +447,45 @@ function OnboardingAddDataSource(): JSX.Element {
...setupStepItemsBase.slice(2),
]);
} else if (step === 3) {
let targetPath: string;
switch (selectedDataSource?.module) {
case 'apm':
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
break;
case 'logs':
history.push(ROUTES.LOGS);
targetPath = ROUTES.LOGS;
break;
case 'metrics':
history.push(ROUTES.METRICS_EXPLORER);
targetPath = ROUTES.METRICS_EXPLORER;
break;
case 'dashboards':
history.push(ROUTES.ALL_DASHBOARD);
targetPath = ROUTES.ALL_DASHBOARD;
break;
case 'infra-monitoring-hosts':
history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS);
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS;
break;
case 'infra-monitoring-k8s':
history.push(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES);
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES;
break;
case 'messaging-queues-kafka':
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
targetPath = ROUTES.MESSAGING_QUEUES_KAFKA;
break;
case 'messaging-queues-celery':
history.push(ROUTES.MESSAGING_QUEUES_CELERY_TASK);
targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK;
break;
case 'integrations':
history.push(ROUTES.INTEGRATIONS);
targetPath = ROUTES.INTEGRATIONS;
break;
case 'home':
history.push(ROUTES.HOME);
targetPath = ROUTES.HOME;
break;
case 'api-monitoring':
history.push(ROUTES.API_MONITORING);
targetPath = ROUTES.API_MONITORING;
break;
default:
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
}
navigateToPage(targetPath, history.push, event);
}
};
@@ -628,7 +634,7 @@ function OnboardingAddDataSource(): JSX.Element {
<X
size={14}
className="onboarding-header-container-close-icon"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
{
@@ -636,7 +642,7 @@ function OnboardingAddDataSource(): JSX.Element {
},
);
history.push(ROUTES.HOME);
navigateToPage(ROUTES.HOME, history.push, e);
}}
/>
<Typography.Text>Get Started (2/4)</Typography.Text>
@@ -963,7 +969,7 @@ function OnboardingAddDataSource(): JSX.Element {
type="primary"
disabled={!selectedDataSource}
shape="round"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
{
@@ -977,7 +983,7 @@ function OnboardingAddDataSource(): JSX.Element {
selectedEnvironment || selectedFramework || selectedDataSource;
if (currentEntity?.internalRedirect && currentEntity?.link) {
history.push(currentEntity.link);
navigateToPage(currentEntity.link, history.push, e);
} else {
handleUpdateCurrentStep(2);
}
@@ -1048,7 +1054,7 @@ function OnboardingAddDataSource(): JSX.Element {
<Button
type="primary"
shape="round"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
{
@@ -1060,7 +1066,7 @@ function OnboardingAddDataSource(): JSX.Element {
);
handleFilterByCategory('All');
handleUpdateCurrentStep(3);
handleUpdateCurrentStep(3, e);
}}
>
Continue

View File

@@ -65,6 +65,7 @@ import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { useCmdK } from '../../providers/cmdKProvider';
import { routeConfig } from './config';
@@ -306,8 +307,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
icon: <Cog size={16} />,
};
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
const [
@@ -436,10 +435,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
const onClickGetStarted = (event: MouseEvent): void => {
logEvent('Sidebar: Menu clicked', {
menuRoute: '/get-started',
@@ -450,7 +445,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
if (isCtrlMetaKey(event)) {
if (isModifierKeyPressed(event)) {
openInNewTab(onboaringRoute);
} else {
history.push(onboaringRoute);
@@ -465,7 +460,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlMetaKey(event)) {
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {
@@ -664,7 +659,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
if (item.key === ROUTES.SETTINGS) {
if (isCtrlMetaKey(event)) {
if (isModifierKeyPressed(event)) {
openInNewTab(settingsRoute);
} else {
history.push(settingsRoute);
@@ -842,6 +837,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleHelpSupportMenuItemClick = (info: SidebarItem): void => {
const item = helpSupportDropdownMenuItems.find(
(item) => !('type' in item) && item.key === info.key,
@@ -851,6 +847,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
window.open(item.url, '_blank');
}
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
if (item && !('type' in item)) {
logEvent('Help Popover: Item clicked', {
menuRoute: item.key,
@@ -859,10 +857,18 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
switch (item.key) {
case ROUTES.SHORTCUTS:
history.push(ROUTES.SHORTCUTS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SHORTCUTS);
} else {
history.push(ROUTES.SHORTCUTS);
}
break;
case 'invite-collaborators':
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
} else {
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
}
break;
case 'chat-support':
if (window.pylon) {
@@ -881,6 +887,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleSettingsMenuItemClick = (info: SidebarItem): void => {
const item = userSettingsDropdownMenuItems.find(
(item) => item?.key === info.key,
@@ -898,15 +905,30 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
menuRoute: item?.key,
menuLabel,
});
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
switch (info.key) {
case 'account':
history.push(ROUTES.MY_SETTINGS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.MY_SETTINGS);
} else {
history.push(ROUTES.MY_SETTINGS);
}
break;
case 'workspace':
history.push(ROUTES.SETTINGS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SETTINGS);
} else {
history.push(ROUTES.SETTINGS);
}
break;
case 'license':
history.push(ROUTES.LIST_LICENSES);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.LIST_LICENSES);
} else {
history.push(ROUTES.LIST_LICENSES);
}
break;
case 'logout':
Logout();

View File

@@ -0,0 +1,106 @@
/**
* Tests for useSafeNavigate's mock contract.
*
* The real useSafeNavigate hook is globally replaced by a mock via
* jest.config.ts moduleNameMapper, so we cannot test the real
* implementation here. Instead we verify:
*
* 1. The mock accepts the new `event` and `newTab` options without
* type errors — ensuring component tests that pass these options
* won't break.
* 2. The shouldOpenNewTab decision logic (extracted inline below)
* matches the real hook's behaviour.
*/
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isModifierKeyPressed } from 'utils/navigation';
/**
* Mirrors the shouldOpenNewTab logic from the real useSafeNavigate hook.
* Kept in sync manually — any drift will be caught by integration tests.
*/
interface NavigateOptions {
newTab?: boolean;
event?: MouseEvent | React.MouseEvent;
}
const shouldOpenNewTab = (options?: NavigateOptions): boolean =>
Boolean(
options?.newTab || (options?.event && isModifierKeyPressed(options.event)),
);
describe('useSafeNavigate mock contract', () => {
it('mock returns a safeNavigate function', () => {
const { safeNavigate } = useSafeNavigate();
expect(typeof safeNavigate).toBe('function');
});
it('safeNavigate accepts string path with event option', () => {
const { safeNavigate } = useSafeNavigate();
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
expect(() => {
safeNavigate('/alerts', { event });
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith('/alerts', { event });
});
it('safeNavigate accepts string path with newTab option', () => {
const { safeNavigate } = useSafeNavigate();
expect(() => {
safeNavigate('/dashboard', { newTab: true });
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith('/dashboard', { newTab: true });
});
it('safeNavigate accepts SafeNavigateParams with event option', () => {
const { safeNavigate } = useSafeNavigate();
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
expect(() => {
safeNavigate({ pathname: '/settings', search: '?tab=general' }, { event });
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith(
{ pathname: '/settings', search: '?tab=general' },
{ event },
);
});
});
describe('shouldOpenNewTab decision logic', () => {
it('returns true when newTab is true', () => {
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
});
it('returns true when event has metaKey pressed', () => {
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
expect(shouldOpenNewTab({ event })).toBe(true);
});
it('returns true when event has ctrlKey pressed', () => {
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
expect(shouldOpenNewTab({ event })).toBe(true);
});
it('returns false when event has no modifier keys', () => {
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
expect(shouldOpenNewTab({ event })).toBe(false);
});
it('returns false when no options provided', () => {
expect(shouldOpenNewTab()).toBe(false);
expect(shouldOpenNewTab(undefined)).toBe(false);
});
it('returns false when options provided without event or newTab', () => {
expect(shouldOpenNewTab({})).toBe(false);
});
it('newTab takes precedence even without event', () => {
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
});
});

View File

@@ -0,0 +1,94 @@
import { act, renderHook } from '@testing-library/react';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsTextSelected } from 'hooks/useIsTextSelected';
import { ILog } from 'types/api/logs/log';
import useLogDetailHandlers from '../useLogDetailHandlers';
jest.mock('hooks/logs/useActiveLog');
jest.mock('hooks/useIsTextSelected');
const mockOnSetActiveLog = jest.fn();
const mockOnClearActiveLog = jest.fn();
const mockOnAddToQuery = jest.fn();
const mockOnGroupByAttribute = jest.fn();
const mockIsTextSelected = jest.fn();
const mockLog: ILog = {
id: 'log-1',
timestamp: '2024-01-01T00:00:00Z',
date: '2024-01-01',
body: 'test log body',
severityText: 'INFO',
severityNumber: 9,
traceFlags: 0,
traceId: '',
spanID: '',
attributesString: {},
attributesInt: {},
attributesFloat: {},
resources_string: {},
scope_string: {},
attributes_string: {},
severity_text: '',
severity_number: 0,
};
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(useIsTextSelected).mockReturnValue(mockIsTextSelected);
jest.mocked(useActiveLog).mockReturnValue({
activeLog: null,
onSetActiveLog: mockOnSetActiveLog,
onClearActiveLog: mockOnClearActiveLog,
onAddToQuery: mockOnAddToQuery,
onGroupByAttribute: mockOnGroupByAttribute,
});
});
it('should not open log detail when text is selected', () => {
mockIsTextSelected.mockReturnValue(true);
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
});
it('should open log detail when no text is selected', () => {
mockIsTextSelected.mockReturnValue(false);
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnSetActiveLog).toHaveBeenCalledWith(mockLog);
});
it('should toggle off when clicking the same active log', () => {
mockIsTextSelected.mockReturnValue(false);
jest.mocked(useActiveLog).mockReturnValue({
activeLog: mockLog,
onSetActiveLog: mockOnSetActiveLog,
onClearActiveLog: mockOnClearActiveLog,
onAddToQuery: mockOnAddToQuery,
onGroupByAttribute: mockOnGroupByAttribute,
});
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnClearActiveLog).toHaveBeenCalled();
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
});

View File

@@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import type { UseActiveLog } from 'hooks/logs/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsTextSelected } from 'hooks/useIsTextSelected';
import { ILog } from 'types/api/logs/log';
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
@@ -28,9 +29,13 @@ function useLogDetailHandlers({
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
const isTextSelected = useIsTextSelected();
const handleSetActiveLog = useCallback(
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
if (isTextSelected()) {
return;
}
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
@@ -39,7 +44,7 @@ function useLogDetailHandlers({
onSetActiveLog(log);
setSelectedTab(nextTab ?? defaultTab);
},
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog, isTextSelected],
);
const handleCloseLogDetail = useCallback((): void => {

View File

@@ -0,0 +1,10 @@
import { useCallback } from 'react';
export function useIsTextSelected(): () => boolean {
return useCallback((): boolean => {
const selection = window.getSelection();
return (
!!selection && !selection.isCollapsed && selection.toString().length > 0
);
}, []);
}

View File

@@ -1,11 +1,13 @@
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { cloneDeep, isEqual } from 'lodash-es';
import { isModifierKeyPressed } from 'utils/navigation';
interface NavigateOptions {
replace?: boolean;
state?: any;
newTab?: boolean;
event?: MouseEvent | React.MouseEvent;
}
interface SafeNavigateParams {
@@ -105,6 +107,7 @@ export const useSafeNavigate = (
const location = useLocation();
const safeNavigate = useCallback(
// eslint-disable-next-line sonarjs/cognitive-complexity
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
const currentUrl = new URL(
`${location.pathname}${location.search}`,
@@ -122,8 +125,10 @@ export const useSafeNavigate = (
);
}
// If newTab is true, open in new tab and return early
if (options?.newTab) {
const shouldOpenNewTab =
options?.newTab || (options?.event && isModifierKeyPressed(options.event));
if (shouldOpenNewTab) {
const targetPath =
typeof to === 'string'
? to

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Breadcrumb, Button, Divider } from 'antd';
@@ -18,6 +18,7 @@ import {
NEW_ALERT_SCHEMA_VERSION,
PostableAlertRuleV2,
} from 'types/api/alerts/alertTypesV2';
import { navigateToPage } from 'utils/navigation';
import AlertHeader from './AlertHeader/AlertHeader';
import AlertNotFound from './AlertNotFound';
@@ -58,11 +59,11 @@ function BreadCrumbItem({
if (isLast) {
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
}
const handleNavigate = (): void => {
const handleNavigate = (e: React.MouseEvent): void => {
if (!route) {
return;
}
history.push(ROUTES.LIST_ALL_ALERT);
navigateToPage(ROUTES.LIST_ALL_ALERT, history.push, e);
};
return (

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { Button, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -15,8 +16,8 @@ function AlertNotFound({ isTestAlert }: AlertNotFoundProps): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { safeNavigate } = useSafeNavigate();
const checkAllRulesHandler = (): void => {
safeNavigate(ROUTES.LIST_ALL_ALERT);
const checkAllRulesHandler = (e?: React.MouseEvent): void => {
safeNavigate(ROUTES.LIST_ALL_ALERT, { event: e });
};
const contactSupportHandler = (): void => {

View File

@@ -69,7 +69,10 @@ describe('AlertNotFound', () => {
const user = userEvent.setup();
render(<AlertNotFound isTestAlert={false} />);
await user.click(screen.getByText('Check all rules'));
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.LIST_ALL_ALERT);
expect(mockSafeNavigate).toHaveBeenCalledWith(
ROUTES.LIST_ALL_ALERT,
expect.objectContaining({ event: expect.any(Object) }),
);
});
it('should navigate to the correct support page for cloud users when button is clicked', async () => {

View File

@@ -18,7 +18,7 @@ function AlertTypeSelectionPage(): JSX.Element {
}, []);
const handleSelectType = useCallback(
(type: AlertTypes): void => {
(type: AlertTypes, event?: React.MouseEvent): void => {
// For anamoly based alert, we need to set the ruleType to anomaly_rule
// and alertType to metrics_based_alert
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
@@ -41,7 +41,7 @@ function AlertTypeSelectionPage(): JSX.Element {
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
}
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { event });
},
[queryParams, safeNavigate],
);

View File

@@ -1,14 +1,14 @@
import { useHistory } from 'react-router-dom';
import { Button, Flex, Typography } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { ArrowRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { navigateToPage } from 'utils/navigation';
import { routePermission } from 'utils/permission';
import './Integrations.styles.scss';
function Header(): JSX.Element {
const history = useHistory();
const { user } = useAppContext();
const isGetStartedWithCloudAllowed = routePermission.GET_STARTED_WITH_CLOUD.includes(
@@ -30,7 +30,9 @@ function Header(): JSX.Element {
<Button
className="periscope-btn primary view-data-sources-btn"
type="primary"
onClick={(): void => history.push(ROUTES.GET_STARTED_WITH_CLOUD)}
onClick={(e): void =>
navigateToPage(ROUTES.GET_STARTED_WITH_CLOUD, history.push, e)
}
>
<span>View 150+ Data Sources</span>
<ArrowRight size={14} />

View File

@@ -7,6 +7,7 @@ import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import useUrlQuery from 'hooks/useUrlQuery';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import {
MessagingQueuesViewType,
@@ -64,8 +65,14 @@ function MQDetailPage(): JSX.Element {
selectedView !== MessagingQueuesViewType.dropRate.value &&
selectedView !== MessagingQueuesViewType.metricPage.value;
const handleBackClick = (): void => {
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
const handleBackClick = (
event?: React.MouseEvent | React.KeyboardEvent,
): void => {
if (event && isModifierKeyPressed(event as React.MouseEvent)) {
openInNewTab(ROUTES.MESSAGING_QUEUES_KAFKA);
} else {
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
}
};
return (
@@ -77,7 +84,7 @@ function MQDetailPage(): JSX.Element {
className="message-queue-text"
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleBackClick();
handleBackClick(e);
}
}}
role="button"

View File

@@ -30,6 +30,7 @@ import {
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { formatNumericValue } from 'utils/numericUtils';
import { getTableDataForProducerLatencyOverview } from './MQTableUtils';
@@ -80,7 +81,12 @@ export function getColumns(
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
history.push(`/services/${encodeURIComponent(text)}`);
const path = `/services/${encodeURIComponent(text)}`;
if (isModifierKeyPressed(e)) {
openInNewTab(path);
} else {
history.push(path);
}
}}
>
{text}

View File

@@ -10,6 +10,7 @@ import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import {
KAFKA_SETUP_DOC_LINK,
@@ -23,26 +24,40 @@ function MessagingQueues(): JSX.Element {
const history = useHistory();
const { t } = useTranslation('messagingQueuesKafkaOverview');
const redirectToDetailsPage = (callerView?: string): void => {
const redirectToDetailsPage = (
callerView?: string,
event?: React.MouseEvent,
): void => {
logEvent('Messaging Queues: View details clicked', {
page: 'Messaging Queues Overview',
source: callerView,
});
history.push(
`${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
);
const path = `${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`;
if (event && isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
history.push(path);
}
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const getStartedRedirect = (link: string, sourceCard: string): void => {
const getStartedRedirect = (
link: string,
sourceCard: string,
event?: React.MouseEvent,
): void => {
logEvent('Messaging Queues: Get started clicked', {
source: sourceCard,
link: isCloudUserVal ? link : KAFKA_SETUP_DOC_LINK,
});
if (isCloudUserVal) {
history.push(link);
if (event && isModifierKeyPressed(event)) {
openInNewTab(link);
} else {
history.push(link);
}
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
@@ -79,10 +94,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
onClick={(e): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
'Configure Consumer',
e,
)
}
>
@@ -98,10 +114,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
onClick={(e): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
'Configure Producer',
e,
)
}
>
@@ -117,10 +134,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
onClick={(e): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
'Monitor kafka',
e,
)
}
>
@@ -143,8 +161,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value, e)
}
>
{t('summarySection.viewDetailsButton')}
@@ -161,8 +179,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value, e)
}
>
{t('summarySection.viewDetailsButton')}
@@ -179,8 +197,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
onClick={(e): void =>
redirectToDetailsPage(
MessagingQueuesViewType.partitionLatency.value,
e,
)
}
>
{t('summarySection.viewDetailsButton')}
@@ -197,8 +218,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value, e)
}
>
{t('summarySection.viewDetailsButton')}
@@ -215,8 +236,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value, e)
}
>
{t('summarySection.viewDetailsButton')}

View File

@@ -16,6 +16,7 @@ import history from 'lib/history';
import { Cog } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { getRoutes } from './utils';
@@ -188,12 +189,6 @@ function SettingsPage(): JSX.Element {
],
);
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
const onClickHandler = useCallback(
(key: string, event: MouseEvent | null) => {
const params = new URLSearchParams(search);
@@ -202,7 +197,7 @@ function SettingsPage(): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlMetaKey(event)) {
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {

View File

@@ -1,5 +1,5 @@
/* eslint-disable react/no-unescaped-entities */
import { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import type { TabsProps } from 'antd';
@@ -27,6 +27,7 @@ import { CircleArrowRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { navigateToPage } from 'utils/navigation';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
@@ -133,10 +134,10 @@ export default function WorkspaceBlocked(): JSX.Element {
});
};
const handleViewBilling = (): void => {
const handleViewBilling = (e?: React.MouseEvent): void => {
logEvent('Workspace Blocked: User Clicked View Billing', {});
history.push(ROUTES.BILLING);
navigateToPage(ROUTES.BILLING, history.push, e);
};
const renderCustomerStories = (
@@ -296,7 +297,7 @@ export default function WorkspaceBlocked(): JSX.Element {
type="link"
size="small"
role="button"
onClick={handleViewBilling}
onClick={(e): void => handleViewBilling(e)}
>
View Billing
</Button>

View File

@@ -0,0 +1,123 @@
import {
isModifierKeyPressed,
navigateToPage,
openInNewTab,
} from '../navigation';
describe('navigation utilities', () => {
const originalWindowOpen = window.open;
beforeEach(() => {
window.open = jest.fn();
});
afterEach(() => {
window.open = originalWindowOpen;
});
describe('isModifierKeyPressed', () => {
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
({
metaKey: false,
ctrlKey: false,
...overrides,
} as MouseEvent);
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
const event = createMouseEvent({ metaKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
const event = createMouseEvent({ ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns true when both metaKey and ctrlKey are pressed', () => {
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns false when neither modifier key is pressed', () => {
const event = createMouseEvent();
expect(isModifierKeyPressed(event)).toBe(false);
});
it('returns false when only shiftKey or altKey are pressed', () => {
const event = createMouseEvent({
shiftKey: true,
altKey: true,
} as Partial<MouseEvent>);
expect(isModifierKeyPressed(event)).toBe(false);
});
});
describe('openInNewTab', () => {
it('calls window.open with the given path and _blank target', () => {
openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
});
it('handles full URLs', () => {
openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('handles paths with query strings', () => {
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(window.open).toHaveBeenCalledWith(
'/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
});
});
describe('navigateToPage', () => {
const mockNavigate = jest.fn() as jest.MockedFunction<(path: string) => void>;
beforeEach(() => {
mockNavigate.mockClear();
});
it('opens new tab when metaKey is pressed', () => {
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
navigateToPage('/services', mockNavigate, event);
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
expect(mockNavigate).not.toHaveBeenCalled();
});
it('opens new tab when ctrlKey is pressed', () => {
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
navigateToPage('/services', mockNavigate, event);
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
expect(mockNavigate).not.toHaveBeenCalled();
});
it('calls navigate callback when no modifier key is pressed', () => {
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
navigateToPage('/services', mockNavigate, event);
expect(mockNavigate).toHaveBeenCalledWith('/services');
expect(window.open).not.toHaveBeenCalled();
});
it('calls navigate callback when no event is provided', () => {
navigateToPage('/services', mockNavigate);
expect(mockNavigate).toHaveBeenCalledWith('/services');
expect(window.open).not.toHaveBeenCalled();
});
it('calls navigate callback when event is undefined', () => {
navigateToPage('/dashboard', mockNavigate, undefined);
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
expect(window.open).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,37 @@
import React from 'react';
/**
* Returns true if the user is holding Cmd (Mac) or Ctrl (Windows/Linux)
* during a click event — the universal "open in new tab" modifier.
*/
export const isModifierKeyPressed = (
event: MouseEvent | React.MouseEvent,
): boolean => event.metaKey || event.ctrlKey;
/**
* Opens the given path in a new browser tab.
*/
export const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
/**
* Navigates to a path, respecting modifier keys. If Cmd/Ctrl is held,
* the path is opened in a new tab. Otherwise, the provided `navigate`
* callback is invoked for SPA navigation.
*
* @param path - The target URL path
* @param navigate - SPA navigation callback (e.g. history.push, safeNavigate)
* @param event - Optional mouse event to check for modifier keys
*/
export const navigateToPage = (
path: string,
navigate: (path: string) => void,
event?: MouseEvent | React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
navigate(path);
}
};

View File

@@ -120,6 +120,8 @@ func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRange
}
}
resultData.Rows = filteredRows
case *qbtypes.ScalarData:
resultData.Data = filterScalarDataIPs(resultData.Columns, resultData.Data)
}
filteredData = append(filteredData, result)
@@ -145,6 +147,39 @@ func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
return true
}
func filterScalarDataIPs(columns []*qbtypes.ColumnDescriptor, data [][]any) [][]any {
// Find column indices for server address fields
serverColIndices := make([]int, 0)
for i, col := range columns {
if col.Name == derivedKeyHTTPHost {
serverColIndices = append(serverColIndices, i)
}
}
if len(serverColIndices) == 0 {
return data
}
filtered := make([][]any, 0, len(data))
for _, row := range data {
includeRow := true
for _, colIdx := range serverColIndices {
if colIdx < len(row) {
if strVal, ok := row[colIdx].(string); ok {
if net.ParseIP(strVal) != nil {
includeRow = false
break
}
}
}
}
if includeRow {
filtered = append(filtered, row)
}
}
return filtered
}
func shouldIncludeRow(row *qbtypes.RawRow) bool {
if row.Data != nil {
if domainVal, ok := row.Data[derivedKeyHTTPHost]; ok {

View File

@@ -117,6 +117,59 @@ func TestFilterResponse(t *testing.T) {
},
},
},
{
name: "should filter out IP addresses from scalar data",
input: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.ScalarData{
QueryName: "endpoints",
Columns: []*qbtypes.ColumnDescriptor{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Type: qbtypes.ColumnTypeGroup,
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
Type: qbtypes.ColumnTypeAggregation,
},
},
Data: [][]any{
{"192.168.1.1", 10},
{"example.com", 20},
{"10.0.0.1", 5},
},
},
},
},
},
},
expected: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.ScalarData{
QueryName: "endpoints",
Columns: []*qbtypes.ColumnDescriptor{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Type: qbtypes.ColumnTypeGroup,
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
Type: qbtypes.ColumnTypeAggregation,
},
},
Data: [][]any{
{"example.com", 20},
},
},
},
},
},
},
},
}
for _, tt := range tests {

View File

@@ -169,6 +169,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddAnonymousPublicDashboardTransactionFactory(sqlstore),
sqlmigration.NewAddRootUserFactory(sqlstore, sqlschema),
sqlmigration.NewAddUserEmailOrgIDIndexFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateRulesV4ToV5Factory(sqlstore, telemetryStore),
)
}

View File

@@ -0,0 +1,209 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"log/slog"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type migrateRulesV4ToV5 struct {
store sqlstore.SQLStore
telemetryStore telemetrystore.TelemetryStore
logger *slog.Logger
}
func NewMigrateRulesV4ToV5Factory(
store sqlstore.SQLStore,
telemetryStore telemetrystore.TelemetryStore,
) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("migrate_rules_post_deprecation"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &migrateRulesV4ToV5{
store: store,
telemetryStore: telemetryStore,
logger: ps.Logger,
}, nil
})
}
func (migration *migrateRulesV4ToV5) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *migrateRulesV4ToV5) getLogDuplicateKeys(ctx context.Context) ([]string, error) {
query := `
SELECT name
FROM (
SELECT DISTINCT name FROM signoz_logs.distributed_logs_attribute_keys
INTERSECT
SELECT DISTINCT name FROM signoz_logs.distributed_logs_resource_keys
)
ORDER BY name
`
rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query)
if err != nil {
migration.logger.WarnContext(ctx, "failed to query log duplicate keys", "error", err)
return nil, nil
}
defer rows.Close()
var keys []string
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
migration.logger.WarnContext(ctx, "failed to scan log duplicate key", "error", err)
continue
}
keys = append(keys, key)
}
return keys, nil
}
func (migration *migrateRulesV4ToV5) getTraceDuplicateKeys(ctx context.Context) ([]string, error) {
query := `
SELECT tagKey
FROM signoz_traces.distributed_span_attributes_keys
WHERE tagType IN ('tag', 'resource')
GROUP BY tagKey
HAVING COUNT(DISTINCT tagType) > 1
ORDER BY tagKey
`
rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query)
if err != nil {
migration.logger.WarnContext(ctx, "failed to query trace duplicate keys", "error", err)
return nil, nil
}
defer rows.Close()
var keys []string
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
migration.logger.WarnContext(ctx, "failed to scan trace duplicate key", "error", err)
continue
}
keys = append(keys, key)
}
return keys, nil
}
func (migration *migrateRulesV4ToV5) Up(ctx context.Context, db *bun.DB) error {
logsKeys, err := migration.getLogDuplicateKeys(ctx)
if err != nil {
return err
}
tracesKeys, err := migration.getTraceDuplicateKeys(ctx)
if err != nil {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
var rules []struct {
ID string `bun:"id"`
Data map[string]any `bun:"data"`
}
err = tx.NewSelect().
Table("rule").
Column("id", "data").
Scan(ctx, &rules)
if err != nil {
if err == sql.ErrNoRows {
return nil
}
return err
}
alertsMigrator := transition.NewAlertMigrateV5(migration.logger, logsKeys, tracesKeys)
count := 0
for _, rule := range rules {
version, _ := rule.Data["version"].(string)
if version == "v5" {
continue
}
if version == "" {
migration.logger.WarnContext(ctx, "unexpected empty version for rule", "rule_id", rule.ID)
}
migration.logger.InfoContext(ctx, "migrating rule v4 to v5", "rule_id", rule.ID, "current_version", version)
// Check if the queries envelope already exists and is non-empty
hasQueriesEnvelope := false
if condition, ok := rule.Data["condition"].(map[string]any); ok {
if compositeQuery, ok := condition["compositeQuery"].(map[string]any); ok {
if queries, ok := compositeQuery["queries"].([]any); ok && len(queries) > 0 {
hasQueriesEnvelope = true
}
}
}
if hasQueriesEnvelope {
// already has queries envelope, just bump version
// this is because user made a mistake of choosing version
migration.logger.InfoContext(ctx, "rule already has queries envelope, bumping version", "rule_id", rule.ID)
rule.Data["version"] = "v5"
} else {
// old format, run full migration
migration.logger.InfoContext(ctx, "rule has old format, running full migration", "rule_id", rule.ID)
updated := alertsMigrator.Migrate(ctx, rule.Data)
if !updated {
migration.logger.WarnContext(ctx, "expected updated to be true but got false", "rule_id", rule.ID)
continue
}
rule.Data["version"] = "v5"
}
dataJSON, err := json.Marshal(rule.Data)
if err != nil {
return err
}
_, err = tx.NewUpdate().
Table("rule").
Set("data = ?", string(dataJSON)).
Where("id = ?", rule.ID).
Exec(ctx)
if err != nil {
return err
}
count++
}
if count != 0 {
migration.logger.InfoContext(ctx, "migrate v4 alerts", "count", count)
}
return tx.Commit()
}
func (migration *migrateRulesV4ToV5) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -355,6 +355,10 @@ func (r *PostableRule) validate() error {
errs = append(errs, signozError.NewInvalidInputf(signozError.CodeInvalidInput, "composite query is required"))
}
if r.Version != "v5" {
errs = append(errs, signozError.NewInvalidInputf(signozError.CodeInvalidInput, "only version v5 is supported, got %q", r.Version))
}
if isAllQueriesDisabled(r.RuleCondition.CompositeQuery) {
errs = append(errs, signozError.NewInvalidInputf(signozError.CodeInvalidInput, "all queries are disabled in rule condition"))
}

View File

@@ -108,6 +108,7 @@ func TestParseIntoRule(t *testing.T) {
"ruleType": "threshold_rule",
"evalWindow": "5m",
"frequency": "1m",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -150,6 +151,7 @@ func TestParseIntoRule(t *testing.T) {
content: []byte(`{
"alert": "DefaultsRule",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -187,6 +189,7 @@ func TestParseIntoRule(t *testing.T) {
initRule: PostableRule{},
content: []byte(`{
"alert": "PromQLRule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "promql",
@@ -256,6 +259,7 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "SeverityLabelTest",
"schemaVersion": "v1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -344,6 +348,7 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "NoLabelsTest",
"schemaVersion": "v1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -384,6 +389,7 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "OverwriteTest",
"schemaVersion": "v1",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -474,6 +480,7 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
content: []byte(`{
"alert": "V2Test",
"schemaVersion": "v2",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -517,6 +524,7 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
initRule: PostableRule{},
content: []byte(`{
"alert": "DefaultSchemaTest",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -569,6 +577,7 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
func TestParseIntoRuleThresholdGeneration(t *testing.T) {
content := []byte(`{
"alert": "TestThresholds",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -639,6 +648,7 @@ func TestParseIntoRuleMultipleThresholds(t *testing.T) {
"schemaVersion": "v2",
"alert": "MultiThresholdAlert",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -732,6 +742,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -766,6 +777,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -799,6 +811,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyAboveTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -833,6 +846,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyAboveTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -866,6 +880,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowAllTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -901,6 +916,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyBelowAllTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -935,6 +951,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "AnomalyOutOfBoundsTest",
"ruleType": "anomaly_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -969,6 +986,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "ThresholdTest",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",
@@ -1003,6 +1021,7 @@ func TestAnomalyNegationEval(t *testing.T) {
ruleJSON: []byte(`{
"alert": "ThresholdTest",
"ruleType": "threshold_rule",
"version": "v5",
"condition": {
"compositeQuery": {
"queryType": "builder",