Compare commits

..

1 Commits

Author SHA1 Message Date
Nagesh Bansal
f605624538 chore: deprecates generator from deploy 2026-02-27 21:51:17 +05:30
71 changed files with 327 additions and 1690 deletions

View File

@@ -1,38 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
extra_hosts:
- host.docker.internal:host-gateway
logging:
options:
max-size: 50m
max-file: "3"
deploy:
restart_policy:
condition: on-failure
services:
hotrod:
<<: *common
image: jaegertracing/example-hotrod:1.61.0
command: [ "all" ]
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 #
load-hotrod:
<<: *common
image: "signoz/locust:1.2.3"
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../../../common/locust-scripts:/locust
networks:
signoz-net:
name: signoz-net
external: true

View File

@@ -1,69 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
extra_hosts:
- host.docker.internal:host-gateway
logging:
options:
max-size: 50m
max-file: "3"
deploy:
mode: global
restart_policy:
condition: on-failure
services:
otel-agent:
<<: *common
image: otel/opentelemetry-collector-contrib:0.111.0
command:
- --config=/etc/otel-collector-config.yaml
volumes:
- ./otel-agent-config.yaml:/etc/otel-collector-config.yaml
- /:/hostfs:ro
environment:
- SIGNOZ_COLLECTOR_ENDPOINT=http://host.docker.internal:4317 # In case of external SigNoz or cloud, update the endpoint and access token
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
# - SIGNOZ_ACCESS_TOKEN="<your-access-token>"
# Before exposing the ports, make sure the ports are not used by other services
# ports:
# - "4317:4317"
# - "4318:4318"
otel-metrics:
<<: *common
image: otel/opentelemetry-collector-contrib:0.111.0
user: 0:0 # If you have security concerns, you can replace this with your `UID:GID` that has necessary permissions to docker.sock
command:
- --config=/etc/otel-collector-config.yaml
volumes:
- ./otel-metrics-config.yaml:/etc/otel-collector-config.yaml
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SIGNOZ_COLLECTOR_ENDPOINT=http://host.docker.internal:4317 # In case of external SigNoz or cloud, update the endpoint and access token
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
# - SIGNOZ_ACCESS_TOKEN="<your-access-token>"
# Before exposing the ports, make sure the ports are not used by other services
# ports:
# - "4317:4317"
# - "4318:4318"
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
logspout:
<<: *common
image: "gliderlabs/logspout:v3.2.14"
command: syslog+tcp://otel-agent:2255
user: root
volumes:
- /etc/hostname:/etc/host_hostname:ro
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- otel-agent
networks:
signoz-net:
name: signoz-net
external: true

View File

@@ -1,102 +0,0 @@
receivers:
hostmetrics:
collection_interval: 30s
root_path: /hostfs
scrapers:
cpu: {}
load: {}
memory: {}
disk: {}
filesystem: {}
network: {}
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-agent
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-agent
tcplog/docker:
listen_address: "0.0.0.0:2255"
operators:
- type: regex_parser
regex: '^<([0-9]+)>[0-9]+ (?P<timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?) (?P<container_id>\S+) (?P<container_name>\S+) [0-9]+ - -( (?P<body>.*))?'
timestamp:
parse_from: attributes.timestamp
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
- type: move
from: attributes["body"]
to: body
- type: remove
field: attributes.timestamp
# please remove names from below if you want to collect logs from them
- type: filter
id: signoz_logs_filter
expr: 'attributes.container_name matches "^(signoz_(logspout|signoz|otel-collector|clickhouse|zookeeper))|(infra_(logspout|otel-agent|otel-metrics)).*"'
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors:
# - ec2
# - gcp
# - azure
- env
- system
timeout: 2s
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
otlp:
endpoint: ${env:SIGNOZ_COLLECTOR_ENDPOINT}
tls:
insecure: true
headers:
signoz-access-token: ${env:SIGNOZ_ACCESS_TOKEN}
# debug: {}
service:
telemetry:
logs:
encoding: json
metrics:
address: 0.0.0.0:8888
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [resourcedetection, batch]
exporters: [otlp]
metrics:
receivers: [otlp]
processors: [resourcedetection, batch]
exporters: [otlp]
metrics/hostmetrics:
receivers: [hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
metrics/prometheus:
receivers: [prometheus]
processors: [resourcedetection, batch]
exporters: [otlp]
logs:
receivers: [otlp, tcplog/docker]
processors: [resourcedetection, batch]
exporters: [otlp]

View File

@@ -1,103 +0,0 @@
receivers:
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-metrics
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-metrics
# For Docker daemon metrics to be scraped, it must be configured to expose
# Prometheus metrics, as documented here: https://docs.docker.com/config/daemon/prometheus/
# - job_name: docker-daemon
# dockerswarm_sd_configs:
# - host: unix:///var/run/docker.sock
# role: nodes
# relabel_configs:
# - source_labels: [__meta_dockerswarm_node_address]
# target_label: __address__
# replacement: $1:9323
- job_name: "dockerswarm"
dockerswarm_sd_configs:
- host: unix:///var/run/docker.sock
role: tasks
relabel_configs:
- action: keep
regex: running
source_labels:
- __meta_dockerswarm_task_desired_state
- action: keep
regex: true
source_labels:
- __meta_dockerswarm_service_label_signoz_io_scrape
- regex: ([^:]+)(?::\d+)?
replacement: $1
source_labels:
- __address__
target_label: swarm_container_ip
- separator: .
source_labels:
- __meta_dockerswarm_service_name
- __meta_dockerswarm_task_slot
- __meta_dockerswarm_task_id
target_label: swarm_container_name
- target_label: __address__
source_labels:
- swarm_container_ip
- __meta_dockerswarm_service_label_signoz_io_port
separator: ":"
- source_labels:
- __meta_dockerswarm_service_label_signoz_io_path
target_label: __metrics_path__
- source_labels:
- __meta_dockerswarm_service_label_com_docker_stack_namespace
target_label: namespace
- source_labels:
- __meta_dockerswarm_service_name
target_label: service_name
- source_labels:
- __meta_dockerswarm_task_id
target_label: service_instance_id
- source_labels:
- __meta_dockerswarm_node_hostname
target_label: host_name
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
detectors:
- env
- system
timeout: 2s
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
otlp:
endpoint: ${env:SIGNOZ_COLLECTOR_ENDPOINT}
tls:
insecure: true
headers:
signoz-access-token: ${env:SIGNOZ_ACCESS_TOKEN}
# debug: {}
service:
telemetry:
logs:
encoding: json
metrics:
address: 0.0.0.0:8888
extensions:
- health_check
- pprof
pipelines:
metrics:
receivers: [prometheus]
processors: [resourcedetection, batch]
exporters: [otlp]

View File

@@ -82,12 +82,6 @@ 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:
@@ -99,19 +93,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, metadataexporter, signozmeter]
exporters: [clickhousetraces, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
exporters: [clickhouselogsexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -1,39 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
extra_hosts:
- host.docker.internal:host-gateway
logging:
options:
max-size: 50m
max-file: "3"
restart: unless-stopped
services:
hotrod:
<<: *common
image: jaegertracing/example-hotrod:1.61.0
container_name: hotrod
command: [ "all" ]
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 # In case of external SigNoz or cloud, update the endpoint and access token
# - OTEL_OTLP_HEADERS=signoz-access-token=<your-access-token>
load-hotrod:
<<: *common
image: "signoz/locust:1.2.3"
container_name: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../../../common/locust-scripts:/locust
networks:
signoz-net:
name: signoz-net
external: true

View File

@@ -1,43 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
extra_hosts:
- host.docker.internal:host-gateway
logging:
options:
max-size: 50m
max-file: "3"
restart: unless-stopped
services:
otel-agent:
<<: *common
image: otel/opentelemetry-collector-contrib:0.111.0
command:
- --config=/etc/otel-collector-config.yaml
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- /:/hostfs:ro
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SIGNOZ_COLLECTOR_ENDPOINT=http://host.docker.internal:4317 # In case of external SigNoz or cloud, update the endpoint and access token
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux # Replace signoz-host with the actual hostname
# - SIGNOZ_ACCESS_TOKEN="<your-access-token>"
# Before exposing the ports, make sure the ports are not used by other services
# ports:
# - "4317:4317"
# - "4318:4318"
logspout:
<<: *common
image: "gliderlabs/logspout:v3.2.14"
volumes:
- /etc/hostname:/etc/host_hostname:ro
- /var/run/docker.sock:/var/run/docker.sock
command: syslog+tcp://otel-agent:2255
depends_on:
- otel-agent
networks:
signoz-net:
name: signoz-net
external: true

View File

@@ -1,139 +0,0 @@
receivers:
hostmetrics:
collection_interval: 30s
root_path: /hostfs
scrapers:
cpu: {}
load: {}
memory: {}
disk: {}
filesystem: {}
network: {}
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
# For Docker daemon metrics to be scraped, it must be configured to expose
# Prometheus metrics, as documented here: https://docs.docker.com/config/daemon/prometheus/
# - job_name: docker-daemon
# static_configs:
# - targets:
# - host.docker.internal:9323
# labels:
# job_name: docker-daemon
- job_name: docker-container
docker_sd_configs:
- host: unix:///var/run/docker.sock
relabel_configs:
- action: keep
regex: true
source_labels:
- __meta_docker_container_label_signoz_io_scrape
- regex: true
source_labels:
- __meta_docker_container_label_signoz_io_path
target_label: __metrics_path__
- regex: (.+)
source_labels:
- __meta_docker_container_label_signoz_io_path
target_label: __metrics_path__
- separator: ":"
source_labels:
- __meta_docker_network_ip
- __meta_docker_container_label_signoz_io_port
target_label: __address__
- regex: '/(.*)'
replacement: '$1'
source_labels:
- __meta_docker_container_name
target_label: container_name
- regex: __meta_docker_container_label_signoz_io_(.+)
action: labelmap
replacement: $1
tcplog/docker:
listen_address: "0.0.0.0:2255"
operators:
- type: regex_parser
regex: '^<([0-9]+)>[0-9]+ (?P<timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?) (?P<container_id>\S+) (?P<container_name>\S+) [0-9]+ - -( (?P<body>.*))?'
timestamp:
parse_from: attributes.timestamp
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
- type: move
from: attributes["body"]
to: body
- type: remove
field: attributes.timestamp
# please remove names from below if you want to collect logs from them
- type: filter
id: signoz_logs_filter
expr: 'attributes.container_name matches "^signoz|(signoz-(|otel-collector|clickhouse|zookeeper))|(infra-(logspout|otel-agent)-.*)"'
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors:
# - ec2
# - gcp
# - azure
- env
- system
timeout: 2s
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
otlp:
endpoint: ${env:SIGNOZ_COLLECTOR_ENDPOINT}
tls:
insecure: true
headers:
signoz-access-token: ${env:SIGNOZ_ACCESS_TOKEN}
# debug: {}
service:
telemetry:
logs:
encoding: json
metrics:
address: 0.0.0.0:8888
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [resourcedetection, batch]
exporters: [otlp]
metrics:
receivers: [otlp]
processors: [resourcedetection, batch]
exporters: [otlp]
metrics/hostmetrics:
receivers: [hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
metrics/prometheus:
receivers: [prometheus]
processors: [resourcedetection, batch]
exporters: [otlp]
logs:
receivers: [otlp, tcplog/docker]
processors: [resourcedetection, batch]
exporters: [otlp]

View File

@@ -82,12 +82,6 @@ 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:
@@ -99,19 +93,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, metadataexporter, signozmeter]
exporters: [clickhousetraces, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
exporters: [signozclickhousemetrics, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
exporters: [clickhouselogsexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

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

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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 = (e?: React.MouseEvent): void => {
const handleOpenInExplorer = (): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
@@ -227,9 +227,7 @@ function LogDetailInner({
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
event: e,
});
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(

View File

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

View File

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

View File

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

View File

@@ -161,7 +161,6 @@ 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 React, { useCallback } from 'react';
import { useCallback } from 'react';
import { Button } from 'antd';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -23,19 +23,16 @@ function DashboardBreadcrumbs(): JSX.Element {
const { title = '', image = Base64Icons[0] } = selectedData || {};
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 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 generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
safeNavigate(generatedUrl, { event: e });
},
[listSortOrder, safeNavigate],
);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
safeNavigate(generatedUrl);
}, [listSortOrder, safeNavigate]);
return (
<div className="dashboard-breadcrumbs">

View File

@@ -16,7 +16,6 @@ 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';
@@ -112,19 +111,14 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
}));
const onClickTraceHandler = (event: React.MouseEvent): void => {
const onClickTraceHandler = (): void => {
logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
const path = `/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`;
if (isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
history.push(path);
}
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
};
const logEventCalledRef = useRef(false);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
@@ -329,18 +329,13 @@ function FormAlertRules({
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
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],
);
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]);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { 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, event: React.MouseEvent) => void;
onRowClick: (record: ServicesList) => 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: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
onRow={(record): { onClick: () => void } => ({
onClick: (): void => onRowClick(record),
})}
/>
</div>
@@ -284,11 +284,11 @@ function ServiceMetrics({
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
const handleRowClick = useCallback(
(record: ServicesList, event: React.MouseEvent) => {
(record: ServicesList) => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
},
[safeNavigate],
);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {
onRow={(record): { onClick: () => void } => ({
onClick: (): void => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
},
})}
/>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import {
Skeleton,
@@ -11,7 +11,6 @@ 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 {
@@ -77,16 +76,7 @@ export default function HostsListTable({
[],
);
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;
}
const handleRowClick = (record: HostRowData): void => {
onHostClick(record.hostName);
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.HostEntity,
@@ -192,8 +182,8 @@ export default function HostsListTable({
(record as HostRowData & { key: string }).key ?? record.hostName
}
onChange={handleTableChange}
onRow={(record: HostRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
/>

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +24,6 @@ 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';
@@ -452,19 +451,7 @@ function K8sClustersList({
);
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
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;
}
const handleRowClick = (record: K8sClustersRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedClusterName(record.clusterUID);
@@ -528,19 +515,8 @@ function K8sClustersList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setselectedClusterName(record.clusterUID);
},
className: 'expanded-clickable-row',
@@ -731,10 +707,8 @@ function K8sClustersList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +25,6 @@ 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';
@@ -458,19 +457,7 @@ function K8sDaemonSetsList({
nestedDaemonSetsData,
]);
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;
}
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedDaemonSetUID(record.daemonsetUID);
@@ -534,19 +521,8 @@ function K8sDaemonSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setSelectedDaemonSetUID(record.daemonsetUID);
},
className: 'expanded-clickable-row',
@@ -739,10 +715,8 @@ function K8sDaemonSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +25,6 @@ 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';
@@ -464,19 +463,7 @@ function K8sDeploymentsList({
nestedDeploymentsData,
]);
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;
}
const handleRowClick = (record: K8sDeploymentsRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedDeploymentUID(record.deploymentUID);
@@ -540,19 +527,8 @@ function K8sDeploymentsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setselectedDeploymentUID(record.deploymentUID);
},
className: 'expanded-clickable-row',
@@ -746,10 +722,8 @@ function K8sDeploymentsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +25,6 @@ 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';
@@ -429,16 +428,7 @@ function K8sJobsList({
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
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;
}
const handleRowClick = (record: K8sJobsRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedJobUID(record.jobUID);
@@ -502,16 +492,8 @@ function K8sJobsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setselectedJobUID(record.jobUID);
},
className: 'expanded-clickable-row',
@@ -702,10 +684,8 @@ function K8sJobsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +24,6 @@ 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,19 +459,7 @@ function K8sNamespacesList({
nestedNamespacesData,
]);
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;
}
const handleRowClick = (record: K8sNamespacesRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
@@ -536,19 +523,8 @@ function K8sNamespacesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setselectedNamespaceUID(record.namespaceUID);
},
className: 'expanded-clickable-row',
@@ -740,10 +716,8 @@ function K8sNamespacesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +24,6 @@ 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';
@@ -439,16 +438,7 @@ function K8sNodesList({
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
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;
}
const handleRowClick = (record: K8sNodesRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedNodeUID(record.nodeUID);
@@ -513,19 +503,8 @@ function K8sNodesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setSelectedNodeUID(record.nodeUID);
},
className: 'expanded-clickable-row',
@@ -716,10 +695,8 @@ function K8sNodesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +26,6 @@ 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';
@@ -496,16 +495,7 @@ function K8sPodsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
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;
}
const handleRowClick = (record: K8sPodsRowData): void => {
if (groupBy.length === 0) {
setSelectedPodUID(record.podUID);
setSearchParams({
@@ -625,14 +615,8 @@ function K8sPodsList({
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setSelectedPodUID(record.podUID);
},
className: 'expanded-clickable-row',
@@ -768,8 +752,8 @@ function K8sPodsList({
scroll={{ x: true }}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +25,6 @@ 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';
@@ -461,19 +460,7 @@ function K8sStatefulSetsList({
nestedStatefulSetsData,
]);
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;
}
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedStatefulSetUID(record.statefulsetUID);
@@ -537,19 +524,8 @@ function K8sStatefulSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setselectedStatefulSetUID(record.statefulsetUID);
},
className: 'expanded-clickable-row',
@@ -742,8 +718,8 @@ function K8sStatefulSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record) => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +25,6 @@ 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';
@@ -391,16 +390,7 @@ function K8sVolumesList({
);
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
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;
}
const handleRowClick = (record: K8sVolumesRowData): void => {
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedVolumeUID(record.volumeUID);
@@ -464,19 +454,8 @@ function K8sVolumesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
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;
}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
setselectedVolumeUID(record.volumeUID);
},
className: 'expanded-clickable-row',
@@ -662,10 +641,8 @@ function K8sVolumesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import { useCallback, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -7,7 +7,6 @@ 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';
@@ -37,13 +36,9 @@ export function AlertsEmptyState(): JSX.Element {
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback((e: React.MouseEvent) => {
const onClickNewAlertHandler = useCallback(() => {
setLoading(false);
if (isModifierKeyPressed(e)) {
openInNewTab(ROUTES.ALERTS_NEW);
} else {
history.push(ROUTES.ALERTS_NEW);
}
history.push(ROUTES.ALERTS_NEW);
}, []);
return (

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { ReactNode, 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,
@@ -47,23 +46,12 @@ function Overview({
handleChangeSelectedView,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
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'] = {
@@ -208,7 +196,7 @@ function Overview({
<>
{isSearchVisible && (
<Input
ref={searchInputRef}
autoFocus
placeholder="Search for a field..."
className="search-input"
value={fieldSearchInput}

View File

@@ -1,34 +0,0 @@
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,16 +1,7 @@
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,
@@ -76,13 +67,8 @@ export function getColorsForSeverityLabels(
label: string,
index: number,
): string {
const trimmed = label.trim();
if (!trimmed) {
return Color.BG_SLATE_300;
}
const variantColor = SEVERITY_VARIANT_COLORS[trimmed];
// Check if we have a direct mapping for this severity variant
const variantColor = SEVERITY_VARIANT_COLORS[label.trim()];
if (variantColor) {
return variantColor;
}
@@ -117,8 +103,5 @@ export function getColorsForSeverityLabels(
return Color.BG_SAKURA_500;
}
return (
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
Color.BG_SLATE_400
);
return colors[index % colors.length] || themeColors.red;
}

View File

@@ -111,19 +111,23 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
);
const itemContent = useCallback(
(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}
/>
),
(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>
);
},
[
tableColumns,
onSetActiveLog,
@@ -139,8 +143,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
{tableColumns
.filter((column) => column.key)
.map((column) => {
const isDragColumn =
column.key !== 'expand' && column.key !== 'state-indicator';
const isDragColumn = column.key !== 'expand';
return (
<TableHeaderCellStyled

View File

@@ -31,7 +31,6 @@ 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';
@@ -236,7 +235,7 @@ function Application(): JSX.Element {
timestamp: number,
apmToTraceQuery: Query,
isViewLogsClicked?: boolean,
): ((e: React.MouseEvent) => void) => (e: React.MouseEvent): void => {
): (() => void) => (): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - stepInterval);
@@ -260,11 +259,7 @@ function Application(): JSX.Element {
queryString,
);
if (isModifierKeyPressed(e)) {
openInNewTab(newPath);
} else {
history.push(newPath);
}
history.push(newPath);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[stepInterval],

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ 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';
@@ -26,10 +25,9 @@ function DashboardsAndAlertsPopover({
label: (
<Typography.Link
key={alert.alert_id}
onClick={(e): void => {
onClick={(): void => {
params.set(QueryParams.ruleId, alert.alert_id);
const path = `${ROUTES.ALERT_OVERVIEW}?${params.toString()}`;
navigateToPage(path, history.push, e);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}}
className="dashboards-popover-content-item"
>
@@ -57,12 +55,11 @@ function DashboardsAndAlertsPopover({
label: (
<Typography.Link
key={dashboard.dashboard_id}
onClick={(e): void => {
onClick={(): void => {
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboard_id,
}),
{ event: e },
);
}}
className="dashboards-popover-content-item"

View File

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

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-nested-ternary */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { 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,7 +14,6 @@ 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';
@@ -210,15 +209,7 @@ 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,11 +194,7 @@ describe('MetricsTable', () => {
);
fireEvent.click(screen.getByText('Metric 1'));
expect(mockOpenMetricDetails).toHaveBeenCalledWith(
'metric1',
'list',
expect.any(Object),
);
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
});
it('calls setOrderBy when column header is clicked', () => {

View File

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

View File

@@ -1,10 +1,9 @@
import React, { useCallback, useMemo } from 'react';
import { 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';
@@ -22,30 +21,23 @@ function NewExplorerCTA(): JSX.Element | null {
[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 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 button = useMemo(
() => (
<Button
icon={<Undo size={16} />}
onClick={(e): void => onClickHandler(e)}
onClick={onClickHandler}
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 React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useEffectOnce } from 'react-use';
@@ -17,7 +17,6 @@ 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';
@@ -253,13 +252,9 @@ export default function Onboarding(): JSX.Element {
}
};
const handleNext = (e?: React.MouseEvent): void => {
const handleNext = (): void => {
if (activeStep <= 3) {
navigateToPage(
moduleRouteMap[selectedModule.id as ModulesMap],
history.push,
e,
);
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
}
};
@@ -322,9 +317,9 @@ export default function Onboarding(): JSX.Element {
{activeStep === 1 && (
<div className="onboarding-page">
<div
onClick={(e): void => {
onClick={(): void => {
logEvent('Onboarding V2: Skip Button Clicked', {});
navigateToPage(ROUTES.APPLICATION, history.push, e);
history.push(ROUTES.APPLICATION);
}}
className="skip-to-console"
>
@@ -360,11 +355,7 @@ export default function Onboarding(): JSX.Element {
</div>
</div>
<div className="continue-to-next-step">
<Button
type="primary"
icon={<ArrowRightOutlined />}
onClick={(e): void => handleNext(e)}
>
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
{t('get_started')}
</Button>
</div>
@@ -395,16 +386,17 @@ export default function Onboarding(): JSX.Element {
{activeStep > 1 && (
<div className="stepsContainer">
<ModuleStepsContainer
onReselectModule={(e?: React.MouseEvent): void => {
onReselectModule={(): void => {
setCurrent(current - 1);
setActiveStep(activeStep - 1);
setSelectedModule(useCases.APM);
resetProgress();
const path = isOnboardingV3Enabled
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
navigateToPage(path, history.push, e);
if (isOnboardingV3Enabled) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
history.push(ROUTES.GET_STARTED);
}
}}
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 React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { LoadingOutlined } from '@ant-design/icons';
@@ -23,7 +23,6 @@ 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';
@@ -142,13 +141,13 @@ export default function DataSource(): JSX.Element {
}
};
const goToIntegrationsPage = (e?: React.MouseEvent): void => {
const goToIntegrationsPage = (): void => {
logEvent('Onboarding V2: Go to integrations', {
module: selectedModule?.id,
dataSource: selectedDataSource?.name,
framework: selectedFramework,
});
navigateToPage(ROUTES.INTEGRATIONS, history.push, e);
history.push(ROUTES.INTEGRATIONS);
};
return (
@@ -250,7 +249,7 @@ export default function DataSource(): JSX.Element {
page which allows more sources of sending data
</Typography.Text>
<Button
onClick={(e): void => goToIntegrationsPage(e)}
onClick={goToIntegrationsPage}
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 React, { SetStateAction, useState } from 'react';
import { SetStateAction, useState } from 'react';
import {
ArrowLeftOutlined,
ArrowRightOutlined,
@@ -19,7 +19,6 @@ 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 {
@@ -143,7 +142,7 @@ export default function ModuleStepsContainer({
return true;
};
const redirectToModules = (event?: React.MouseEvent): void => {
const redirectToModules = (): void => {
logEvent('Onboarding V2 Complete', {
module: selectedModule.id,
dataSource: selectedDataSource?.id,
@@ -153,28 +152,26 @@ export default function ModuleStepsContainer({
serviceName,
});
let targetPath: string;
if (selectedModule.id === ModulesMap.APM) {
targetPath = ROUTES.APPLICATION;
history.push(ROUTES.APPLICATION);
} else if (selectedModule.id === ModulesMap.LogsManagement) {
targetPath = ROUTES.LOGS_EXPLORER;
history.push(ROUTES.LOGS_EXPLORER);
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
targetPath = ROUTES.APPLICATION;
history.push(ROUTES.APPLICATION);
} else if (selectedModule.id === ModulesMap.AwsMonitoring) {
targetPath = ROUTES.APPLICATION;
history.push(ROUTES.APPLICATION);
} else {
targetPath = ROUTES.APPLICATION;
history.push(ROUTES.APPLICATION);
}
navigateToPage(targetPath, history.push, event);
};
const handleNext = (event?: React.MouseEvent): void => {
const handleNext = (): void => {
const isValid = isValidForm();
if (isValid) {
if (current === lastStepIndex) {
resetProgress();
redirectToModules(event);
redirectToModules();
return;
}
@@ -382,8 +379,8 @@ export default function ModuleStepsContainer({
}
};
const handleLogoClick = (e: React.MouseEvent): void => {
navigateToPage('/home', history.push, e);
const handleLogoClick = (): void => {
history.push('/home');
};
return (
@@ -403,7 +400,7 @@ export default function ModuleStepsContainer({
style={{ display: 'flex', alignItems: 'center' }}
type="default"
icon={<LeftCircleOutlined />}
onClick={(e): void => onReselectModule(e)}
onClick={onReselectModule}
>
{selectedModule.title}
</Button>
@@ -473,11 +470,7 @@ export default function ModuleStepsContainer({
>
Back
</Button>
<Button
onClick={(e): void => handleNext(e)}
type="primary"
icon={<ArrowRightOutlined />}
>
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
</Button>
<LaunchChatSupport

View File

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

View File

@@ -65,7 +65,6 @@ 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';
@@ -307,6 +306,8 @@ 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 [
@@ -435,6 +436,10 @@ 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',
@@ -445,7 +450,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
if (isModifierKeyPressed(event)) {
if (isCtrlMetaKey(event)) {
openInNewTab(onboaringRoute);
} else {
history.push(onboaringRoute);
@@ -460,7 +465,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isModifierKeyPressed(event)) {
if (event && isCtrlMetaKey(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {
@@ -659,7 +664,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
if (item.key === ROUTES.SETTINGS) {
if (isModifierKeyPressed(event)) {
if (isCtrlMetaKey(event)) {
openInNewTab(settingsRoute);
} else {
history.push(settingsRoute);
@@ -837,7 +842,6 @@ 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,
@@ -847,8 +851,6 @@ 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,
@@ -857,18 +859,10 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
switch (item.key) {
case ROUTES.SHORTCUTS:
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SHORTCUTS);
} else {
history.push(ROUTES.SHORTCUTS);
}
history.push(ROUTES.SHORTCUTS);
break;
case 'invite-collaborators':
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
} else {
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
}
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
break;
case 'chat-support':
if (window.pylon) {
@@ -887,7 +881,6 @@ 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,
@@ -905,30 +898,15 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
menuRoute: item?.key,
menuLabel,
});
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
switch (info.key) {
case 'account':
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.MY_SETTINGS);
} else {
history.push(ROUTES.MY_SETTINGS);
}
history.push(ROUTES.MY_SETTINGS);
break;
case 'workspace':
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SETTINGS);
} else {
history.push(ROUTES.SETTINGS);
}
history.push(ROUTES.SETTINGS);
break;
case 'license':
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.LIST_LICENSES);
} else {
history.push(ROUTES.LIST_LICENSES);
}
history.push(ROUTES.LIST_LICENSES);
break;
case 'logout':
Logout();

View File

@@ -1,106 +0,0 @@
/**
* 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

@@ -1,94 +0,0 @@
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,7 +2,6 @@ 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;
@@ -29,13 +28,9 @@ 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);
@@ -44,7 +39,7 @@ function useLogDetailHandlers({
onSetActiveLog(log);
setSelectedTab(nextTab ?? defaultTab);
},
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog, isTextSelected],
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
);
const handleCloseLogDetail = useCallback((): void => {

View File

@@ -1,10 +0,0 @@
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,13 +1,11 @@
import React, { useCallback } from 'react';
import { 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 {
@@ -107,7 +105,6 @@ 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}`,
@@ -125,10 +122,8 @@ export const useSafeNavigate = (
);
}
const shouldOpenNewTab =
options?.newTab || (options?.event && isModifierKeyPressed(options.event));
if (shouldOpenNewTab) {
// If newTab is true, open in new tab and return early
if (options?.newTab) {
const targetPath =
typeof to === 'string'
? to

View File

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

View File

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

View File

@@ -69,10 +69,7 @@ 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.objectContaining({ event: expect.any(Object) }),
);
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.LIST_ALL_ALERT);
});
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, event?: React.MouseEvent): void => {
(type: AlertTypes): 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()}`, { event });
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);
},
[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,9 +30,7 @@ function Header(): JSX.Element {
<Button
className="periscope-btn primary view-data-sources-btn"
type="primary"
onClick={(e): void =>
navigateToPage(ROUTES.GET_STARTED_WITH_CLOUD, history.push, e)
}
onClick={(): void => history.push(ROUTES.GET_STARTED_WITH_CLOUD)}
>
<span>View 150+ Data Sources</span>
<ArrowRight size={14} />

View File

@@ -7,7 +7,6 @@ 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,
@@ -65,14 +64,8 @@ function MQDetailPage(): JSX.Element {
selectedView !== MessagingQueuesViewType.dropRate.value &&
selectedView !== MessagingQueuesViewType.metricPage.value;
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);
}
const handleBackClick = (): void => {
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
};
return (
@@ -84,7 +77,7 @@ function MQDetailPage(): JSX.Element {
className="message-queue-text"
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleBackClick(e);
handleBackClick();
}
}}
role="button"

View File

@@ -30,7 +30,6 @@ 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';
@@ -81,12 +80,7 @@ export function getColumns(
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
const path = `/services/${encodeURIComponent(text)}`;
if (isModifierKeyPressed(e)) {
openInNewTab(path);
} else {
history.push(path);
}
history.push(`/services/${encodeURIComponent(text)}`);
}}
>
{text}

View File

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

View File

@@ -16,7 +16,6 @@ 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';
@@ -189,6 +188,12 @@ 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);
@@ -197,7 +202,7 @@ function SettingsPage(): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isModifierKeyPressed(event)) {
if (event && isCtrlMetaKey(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {

View File

@@ -1,5 +1,5 @@
/* eslint-disable react/no-unescaped-entities */
import React, { useCallback, useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import type { TabsProps } from 'antd';
@@ -27,7 +27,6 @@ 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';
@@ -134,10 +133,10 @@ export default function WorkspaceBlocked(): JSX.Element {
});
};
const handleViewBilling = (e?: React.MouseEvent): void => {
const handleViewBilling = (): void => {
logEvent('Workspace Blocked: User Clicked View Billing', {});
navigateToPage(ROUTES.BILLING, history.push, e);
history.push(ROUTES.BILLING);
};
const renderCustomerStories = (
@@ -297,7 +296,7 @@ export default function WorkspaceBlocked(): JSX.Element {
type="link"
size="small"
role="button"
onClick={(e): void => handleViewBilling(e)}
onClick={handleViewBilling}
>
View Billing
</Button>

View File

@@ -1,123 +0,0 @@
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

@@ -1,37 +0,0 @@
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);
}
};