Compare commits

..

12 Commits

Author SHA1 Message Date
Yunus M
53004cc02e chore: remove eslint disable 2026-03-16 23:54:46 +05:30
Yunus M
4037010559 refactor: update import order and adjust navigation call in AlertNotFound tests 2026-03-16 23:09:37 +05:30
Yunus M
6d1f80d3cc chore: clean up environment variable usage in login utility and remove unused test file 2026-03-16 23:05:03 +05:30
Yunus M
ace6598410 Merge branch 'main' into chore/support-cmd-click 2026-03-16 22:03:32 +05:30
Yunus M
78814b2639 feat: support opening links in new tab with modifier key in K8sPodsList and SideNav 2026-03-16 22:00:41 +05:30
Yunus M
49d532cc7f refactor: update navigation handling to use safeNavigate with newTab option 2026-03-16 21:32:30 +05:30
Nageshbansal
7371dcacf0 chore: deprecates generator from deploy (#10447) 2026-03-16 14:55:33 +00:00
Abhi kumar
3cdf3e06f3 feat: added chart appearance settings in panel (#10573)
* feat: added section in panel settings

* chore: minor changes

* fix: fixed failing tests

* fix: minor style fixes

* chore: updated the categorisation

* feat: added chart appearance settings in panel

* feat: added fill mode in timeseries

* chore: updated styles + made panel config resizable

* chore: updated styles

* chore: minor styles improvements

* chore: formatting unit section fix

* chore: disabled chart apperance section

* chore: prettier fmt fix

* fix: transform react-resizable-panels in jest config

* fix: failing test

* chore: updated transition timing

* chore: fixed resizable handle styling

* chore: pr review changes

* chore: pr review changes
2026-03-16 14:08:15 +00:00
Yunus M
fe080b6376 feat: enhance navigation tests for middle mouse button interactions 2026-03-09 20:00:34 +05:30
Yunus M
4926481d7f feat: address review comments 2026-03-09 19:54:49 +05:30
Yunus M
32d7216537 chore: update test cases 2026-03-09 19:36:51 +05:30
Yunus M
4ece479c57 chore: support cmd click on all clickable items 2026-03-09 19:36:15 +05:30
113 changed files with 1728 additions and 2082 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

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

@@ -0,0 +1,29 @@
import { PropsWithChildren } from 'react';
type CommonProps = PropsWithChildren<{
className?: string;
minSize?: number;
maxSize?: number;
defaultSize?: number;
direction?: 'horizontal' | 'vertical';
autoSaveId?: string;
withHandle?: boolean;
}>;
export function ResizablePanelGroup({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizablePanel({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizableHandle({ className }: CommonProps): JSX.Element {
return <div className={className} />;
}

View File

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

View File

@@ -14,6 +14,7 @@ const config: Config.InitialOptions = {
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,

View File

@@ -64,7 +64,7 @@
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/toggle-group": "^0.0.1",
"@signozhq/toggle-group": "0.0.1",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable sonarjs/cognitive-complexity */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
@@ -49,6 +50,7 @@ import { AppState } from 'store/reducers';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/navigation';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
@@ -221,7 +223,7 @@ function LogDetailInner({
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (): void => {
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
@@ -234,7 +236,9 @@ function LogDetailInner({
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
newTab: !!e && isModifierKeyPressed(e),
});
};
const handleQueryExpressionChange = useCallback(

View File

@@ -6,6 +6,7 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { isModifierKeyPressed } from 'utils/navigation';
import { getOptionList } from './config';
import { AlertTypeCard, SelectTypeContainer } from './styles';
@@ -70,8 +71,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
</Tag>
) : undefined
}
onClick={(): void => {
onSelect(option.selection);
onClick={(e): void => {
onSelect(option.selection, isModifierKeyPressed(e));
}}
data-testid={`alert-type-card-${option.selection}`}
>
@@ -108,7 +109,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
}
interface SelectAlertTypeProps {
onSelect: (typ: AlertTypes) => void;
onSelect: (type: AlertTypes, newTab?: boolean) => void;
}
export default SelectAlertType;

View File

@@ -4,6 +4,7 @@ import { Button, Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Check, Loader, Send, X } from 'lucide-react';
import { isModifierKeyPressed } from 'utils/navigation';
import { useCreateAlertState } from '../context';
import {
@@ -33,9 +34,9 @@ function Footer(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const handleDiscard = (): void => {
const handleDiscard = (e: React.MouseEvent): void => {
discardAlertRule();
safeNavigate('/alerts');
safeNavigate('/alerts', { newTab: isModifierKeyPressed(e) });
};
const alertValidationMessage = useMemo(

View File

@@ -224,7 +224,7 @@ describe('TimeSeriesPanel utils', () => {
});
});
it('uses DrawStyle.Line and VisibilityMode.Never when series has multiple valid points', () => {
it('uses DrawStyle.Line and showPoints false when series has multiple valid points', () => {
const apiResponse = createApiResponse([
{
metric: {},

View File

@@ -10,9 +10,9 @@ import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DrawStyle,
FillMode,
LineInterpolation,
LineStyle,
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
@@ -124,12 +124,12 @@ export const prepareUPlotConfig = ({
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: true,
lineStyle: LineStyle.Solid,
lineInterpolation: LineInterpolation.Spline,
showPoints: hasSingleValidPoint
? VisibilityMode.Always
: VisibilityMode.Never,
lineStyle: widget.lineStyle || LineStyle.Solid,
lineInterpolation: widget.lineInterpolation || LineInterpolation.Spline,
showPoints:
widget.showPoints || hasSingleValidPoint ? true : !!widget.showPoints,
pointSize: 5,
fillMode: widget.fillMode || FillMode.None,
isDarkMode,
});
});

View File

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

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
@@ -45,6 +45,7 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
import { isModifierKeyPressed } from 'utils/navigation';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
@@ -330,13 +331,18 @@ function FormAlertRules({
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
const onCancelHandler = useCallback(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [safeNavigate, urlQuery]);
const onCancelHandler = useCallback(
(e?: React.MouseEvent) => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
newTab: !!e && isModifierKeyPressed(e),
});
},
[safeNavigate, urlQuery],
);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults

View File

@@ -1,7 +1,13 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import {
LoadingOutlined,
SearchOutlined,
@@ -45,6 +51,7 @@ import { Warning } from 'types/api';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { isModifierKeyPressed } from 'utils/navigation';
import { getLocalStorageGraphVisibilityState } from '../utils';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
@@ -290,9 +297,11 @@ function FullView({
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
if (dashboardEditView) {
safeNavigate(dashboardEditView);
safeNavigate(dashboardEditView, {
newTab: isModifierKeyPressed(e),
});
}
}}
>

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable sonarjs/no-duplicate-string */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
@@ -16,6 +17,7 @@ import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence } from 'motion/react';
@@ -26,6 +28,7 @@ import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { isIngestionActive } from 'utils/app';
import { isModifierKeyPressed } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import AlertRules from './AlertRules/AlertRules';
@@ -44,6 +47,7 @@ const homeInterval = 30 * 60 * 1000;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Home(): JSX.Element {
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const isDarkMode = useIsDarkMode();
const [startTime, setStartTime] = useState<number | null>(null);
@@ -372,11 +376,14 @@ export default function Home(): JSX.Element {
role="button"
tabIndex={0}
className="active-ingestion-card-actions"
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
// eslint-disable-next-line sonarjs/no-duplicate-string
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
safeNavigate(ROUTES.LOGS_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -413,11 +420,13 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
safeNavigate(ROUTES.TRACES_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -454,11 +463,13 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER);
safeNavigate(ROUTES.METRICS_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -508,11 +519,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
safeNavigate(ROUTES.LOGS_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
>
Open Logs Explorer
@@ -522,11 +535,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
safeNavigate(ROUTES.TRACES_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
>
Open Traces Explorer
@@ -536,11 +551,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
safeNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
>
Open Metrics Explorer
@@ -577,11 +594,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
});
history.push(ROUTES.ALL_DASHBOARD);
safeNavigate(ROUTES.ALL_DASHBOARD, {
newTab: isModifierKeyPressed(e),
});
}}
>
Create dashboard
@@ -619,11 +638,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Alerts',
});
history.push(ROUTES.ALERTS_NEW);
safeNavigate(ROUTES.ALERTS_NEW, {
newTab: isModifierKeyPressed(e),
});
}}
>
Create an alert

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { QueryKey } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -30,6 +30,7 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { DOCS_LINKS } from '../constants';
@@ -117,7 +118,7 @@ const ServicesListTable = memo(
onRowClick,
}: {
services: ServicesList[];
onRowClick: (record: ServicesList) => void;
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
}): JSX.Element => (
<div className="services-list-container home-data-item-container metrics-services-list">
<div className="services-list">
@@ -126,8 +127,8 @@ const ServicesListTable = memo(
dataSource={services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => onRowClick(record),
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
})}
/>
</div>
@@ -285,11 +286,13 @@ function ServiceMetrics({
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
const handleRowClick = useCallback(
(record: ServicesList) => {
(record: ServicesList, event: React.MouseEvent) => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, {
newTab: isModifierKeyPressed(event),
});
},
[safeNavigate],
);

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { Link } from 'react-router-dom';
import { Button, Select, Skeleton, Table } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -16,6 +16,7 @@ import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/navigation';
import { DOCS_LINKS } from '../constants';
import { columns, TIME_PICKER_OPTIONS } from './constants';
@@ -173,13 +174,15 @@ export default function ServiceTraces({
dataSource={top5Services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => {
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, {
newTab: isModifierKeyPressed(event),
});
},
})}
/>

View File

@@ -11,6 +11,7 @@ import {
import type { 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 {
@@ -162,7 +163,16 @@ export default function HostsListTable({
[],
);
const handleRowClick = (record: HostRowData): void => {
const handleRowClick = (
record: HostRowData,
event: React.MouseEvent,
): void => {
if (isModifierKeyPressed(event)) {
const params = new URLSearchParams(window.location.search);
params.set('hostName', record.hostName);
openInNewTab(`${window.location.pathname}?${params.toString()}`);
return;
}
onHostClick(record.hostName);
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.HostEntity,
@@ -235,8 +245,8 @@ export default function HostsListTable({
(record as HostRowData & { key: string }).key ?? record.hostName
}
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record: HostRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import { useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import { isModifierKeyPressed } from 'utils/navigation';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
@@ -29,6 +30,7 @@ const alertLogEvents = (
export function AlertsEmptyState(): JSX.Element {
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const [addNewAlert] = useComponentPermission(
['add_new_alert', 'action'],
user.role,
@@ -36,10 +38,13 @@ export function AlertsEmptyState(): JSX.Element {
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback(() => {
setLoading(false);
history.push(ROUTES.ALERTS_NEW);
}, []);
const onClickNewAlertHandler = useCallback(
(e: React.MouseEvent) => {
setLoading(false);
safeNavigate(ROUTES.ALERTS_NEW, { newTab: isModifierKeyPressed(e) });
},
[safeNavigate],
);
return (
<div className="alert-list-container">

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { PlusOutlined } from '@ant-design/icons';
@@ -30,6 +30,7 @@ import { useAppContext } from 'providers/App/App';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
import { isModifierKeyPressed } from 'utils/navigation';
import DeleteAlert from './DeleteAlert';
import { ColumnButton, SearchContainer } from './styles';
@@ -99,16 +100,24 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
});
}, [notificationsApi, t]);
const onClickNewAlertHandler = useCallback(() => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
const onClickNewAlertHandler = useCallback(
(e: React.MouseEvent): void => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, {
newTab: isModifierKeyPressed(e),
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
[],
);
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
const onEditHandler = (
record: GettableAlert,
options?: { newTab?: boolean },
): void => {
const compositeQuery = sanitizeDefaultAlertQuery(
mapQueryDataFromApi(record.condition.compositeQuery),
record.alertType as AlertTypes,
@@ -124,11 +133,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setEditLoader(false);
if (openInNewTab) {
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
} else {
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, {
newTab: options?.newTab,
});
};
const onCloneHandler = (
@@ -265,7 +272,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
e.stopPropagation();
e.preventDefault();
onEditHandler(record, e.metaKey || e.ctrlKey);
onEditHandler(record, { newTab: isModifierKeyPressed(e) });
};
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
@@ -330,7 +337,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
/>,
<ColumnButton
key="2"
onClick={(): void => onEditHandler(record, false)}
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
type="link"
loading={editLoader}
>
@@ -338,7 +347,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</ColumnButton>,
<ColumnButton
key="3"
onClick={(): void => onEditHandler(record, true)}
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>

View File

@@ -82,6 +82,7 @@ import {
Widgets,
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/navigation';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
@@ -372,11 +373,7 @@ function DashboardsList(): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}
safeNavigate(getLink(), { newTab: isModifierKeyPressed(event) });
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
dashboardName: dashboard.name,

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
import React, { Dispatch, SetStateAction, useMemo, useRef } from 'react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
@@ -22,6 +22,7 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace';
import { isModifierKeyPressed } from 'utils/navigation';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -42,7 +43,7 @@ interface OnViewTracePopupClickProps {
apmToTraceQuery: Query;
isViewLogsClicked?: boolean;
stepInterval?: number;
safeNavigate: (url: string) => void;
safeNavigate: (url: string, options?: { newTab?: boolean }) => void;
}
interface OnViewAPIMonitoringPopupClickProps {
@@ -51,8 +52,7 @@ interface OnViewAPIMonitoringPopupClickProps {
stepInterval?: number;
domainName: string;
isError: boolean;
safeNavigate: (url: string) => void;
safeNavigate: (url: string, options?: { newTab?: boolean }) => void;
}
export function generateExplorerPath(
@@ -93,8 +93,8 @@ export function onViewTracePopupClick({
isViewLogsClicked,
stepInterval,
safeNavigate,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
}: OnViewTracePopupClickProps): (e?: React.MouseEvent) => void {
return (e?: React.MouseEvent): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
@@ -118,7 +118,7 @@ export function onViewTracePopupClick({
queryString,
);
safeNavigate(newPath);
safeNavigate(newPath, { newTab: !!e && isModifierKeyPressed(e) });
};
}
@@ -149,8 +149,8 @@ export function onViewAPIMonitoringPopupClick({
isError,
stepInterval,
safeNavigate,
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
return (): void => {
}: OnViewAPIMonitoringPopupClickProps): (e?: React.MouseEvent) => void {
return (e?: React.MouseEvent): void => {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
@@ -190,7 +190,7 @@ export function onViewAPIMonitoringPopupClick({
filters,
);
safeNavigate(newPath);
safeNavigate(newPath, { newTab: !!e && isModifierKeyPressed(e) });
};
}

View File

@@ -115,8 +115,9 @@ function MetricsTable({
onChange: onPaginationChange,
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key, 'list'),
onRow={(record): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void =>
openMetricDetails(record.key, 'list', event),
className: 'clickable-row',
})}
/>

View File

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

View File

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

View File

@@ -18,7 +18,11 @@ export interface MetricsTableProps {
onPaginationChange: (page: number, pageSize: number) => void;
setOrderBy: (orderBy: Querybuildertypesv5OrderByDTO) => void;
totalCount: number;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
queryFilterExpression: Filter;
onFilterChange: (expression: string) => void;
}
@@ -37,7 +41,11 @@ export interface MetricsTreemapProps {
isError: boolean;
error?: APIError;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
setHeatmapView: (value: MetricsexplorertypesTreemapModeDTO) => void;
}
@@ -47,7 +55,11 @@ export interface MetricsTreemapInternalProps {
error?: APIError;
data: MetricsexplorertypesTreemapResponseDTO | undefined;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
}
export interface OrderByPayload {

View File

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

View File

@@ -65,6 +65,35 @@
}
}
.new-widget-container {
.resizable-panel-left-container {
overflow-x: hidden;
overflow-y: auto;
}
.resizable-panel-right-container {
overflow-y: auto !important;
min-width: 350px;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.widget-resizable-panel-group {
.widget-resizable-handle {
height: 100vh;
}
}
}
.lightMode {
.edit-header {
border-bottom: 1px solid var(--bg-vanilla-300);
@@ -81,4 +110,11 @@
}
}
}
.widget-resizable-panel-group {
.bg-border {
background: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300);
}
}
}

View File

@@ -0,0 +1,21 @@
.fill-mode-selector {
.fill-mode-icon {
width: 24px;
height: 24px;
}
.fill-mode-label {
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
color: var(--bg-vanilla-400);
}
}
.lightMode {
.fill-mode-selector {
.fill-mode-label {
color: var(--bg-ink-400);
}
}
}

View File

@@ -0,0 +1,94 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Typography } from 'antd';
import { FillMode } from 'lib/uPlotV2/config/types';
import './FillModeSelector.styles.scss';
interface FillModeSelectorProps {
value: FillMode;
onChange: (value: FillMode) => void;
}
export function FillModeSelector({
value,
onChange,
}: FillModeSelectorProps): JSX.Element {
return (
<section className="fill-mode-selector control-container">
<Typography.Text className="section-heading">Fill mode</Typography.Text>
<ToggleGroup
type="single"
value={value}
variant="outline"
size="lg"
onValueChange={(newValue): void => {
if (newValue) {
onChange(newValue as FillMode);
}
}}
>
<ToggleGroupItem value={FillMode.None} aria-label="None" title="None">
<svg
className="fill-mode-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="8" y="16" width="32" height="16" stroke="#888" fill="none" />
</svg>
<Typography.Text className="section-heading-small">None</Typography.Text>
</ToggleGroupItem>
<ToggleGroupItem value={FillMode.Solid} aria-label="Solid" title="Solid">
<svg
className="fill-mode-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="8" y="16" width="32" height="16" fill="#888" />
</svg>
<Typography.Text className="section-heading-small">Solid</Typography.Text>
</ToggleGroupItem>
<ToggleGroupItem
value={FillMode.Gradient}
aria-label="Gradient"
title="Gradient"
>
<svg
className="fill-mode-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<defs>
<linearGradient id="fill-gradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#888" stopOpacity="0.2" />
<stop offset="100%" stopColor="#888" stopOpacity="0.8" />
</linearGradient>
</defs>
<rect
x="8"
y="16"
width="32"
height="16"
fill="url(#fill-gradient)"
stroke="#888"
/>
</svg>
<Typography.Text className="section-heading-small">
Gradient
</Typography.Text>
</ToggleGroupItem>
</ToggleGroup>
</section>
);
}

View File

@@ -0,0 +1,21 @@
.line-interpolation-selector {
.line-interpolation-icon {
width: 24px;
height: 24px;
}
.line-interpolation-label {
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
color: var(--bg-vanilla-400);
}
}
.lightMode {
.line-interpolation-selector {
.line-interpolation-label {
color: var(--bg-ink-400);
}
}
}

View File

@@ -0,0 +1,110 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Typography } from 'antd';
import { LineInterpolation } from 'lib/uPlotV2/config/types';
import './LineInterpolationSelector.styles.scss';
interface LineInterpolationSelectorProps {
value: LineInterpolation;
onChange: (value: LineInterpolation) => void;
}
export function LineInterpolationSelector({
value,
onChange,
}: LineInterpolationSelectorProps): JSX.Element {
return (
<section className="line-interpolation-selector control-container">
<Typography.Text className="section-heading">
Line interpolation
</Typography.Text>
<ToggleGroup
type="single"
value={value}
variant="outline"
size="lg"
onValueChange={(newValue): void => {
if (newValue) {
onChange(newValue as LineInterpolation);
}
}}
>
<ToggleGroupItem
value={LineInterpolation.Linear}
aria-label="Linear"
title="Linear"
>
<svg
className="line-interpolation-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="8" cy="32" r="3" fill="#888" />
<circle cx="24" cy="16" r="3" fill="#888" />
<circle cx="40" cy="32" r="3" fill="#888" />
<path d="M8 32 L24 16 L40 32" stroke="#888" />
</svg>
</ToggleGroupItem>
<ToggleGroupItem value={LineInterpolation.Spline} aria-label="Spline">
<svg
className="line-interpolation-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="8" cy="32" r="3" fill="#888" />
<circle cx="24" cy="16" r="3" fill="#888" />
<circle cx="40" cy="32" r="3" fill="#888" />
<path d="M8 32 C16 8, 32 8, 40 32" />
</svg>
</ToggleGroupItem>
<ToggleGroupItem
value={LineInterpolation.StepAfter}
aria-label="Step After"
>
<svg
className="line-interpolation-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="8" cy="32" r="3" fill="#888" />
<circle cx="24" cy="16" r="3" fill="#888" />
<circle cx="40" cy="32" r="3" fill="#888" />
<path d="M8 32 V16 H24 V32 H40" />
</svg>
</ToggleGroupItem>
<ToggleGroupItem
value={LineInterpolation.StepBefore}
aria-label="Step Before"
>
<svg
className="line-interpolation-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="8" cy="32" r="3" fill="#888" />
<circle cx="24" cy="16" r="3" fill="#888" />
<circle cx="40" cy="32" r="3" fill="#888" />
<path d="M8 32 H24 V16 H40 V32" />
</svg>
</ToggleGroupItem>
</ToggleGroup>
</section>
);
}

View File

@@ -0,0 +1,21 @@
.line-style-selector {
.line-style-icon {
width: 24px;
height: 24px;
}
.line-style-label {
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
color: var(--bg-vanilla-400);
}
}
.lightMode {
.line-style-selector {
.line-style-label {
color: var(--bg-ink-400);
}
}
}

View File

@@ -0,0 +1,66 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Typography } from 'antd';
import { LineStyle } from 'lib/uPlotV2/config/types';
import './LineStyleSelector.styles.scss';
interface LineStyleSelectorProps {
value: LineStyle;
onChange: (value: LineStyle) => void;
}
export function LineStyleSelector({
value,
onChange,
}: LineStyleSelectorProps): JSX.Element {
return (
<section className="line-style-selector control-container">
<Typography.Text className="section-heading">Line style</Typography.Text>
<ToggleGroup
type="single"
value={value}
variant="outline"
size="lg"
onValueChange={(newValue): void => {
if (newValue) {
onChange(newValue as LineStyle);
}
}}
>
<ToggleGroupItem value={LineStyle.Solid} aria-label="Solid" title="Solid">
<svg
className="line-style-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M8 24 L40 24" />
</svg>
<Typography.Text className="section-heading-small">Solid</Typography.Text>
</ToggleGroupItem>
<ToggleGroupItem
value={LineStyle.Dashed}
aria-label="Dashed"
title="Dashed"
>
<svg
className="line-style-icon"
viewBox="0 0 48 48"
fill="none"
stroke="#888"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="6 4"
>
<path d="M8 24 L40 24" />
</svg>
<Typography.Text className="section-heading-small">Dashed</Typography.Text>
</ToggleGroupItem>
</ToggleGroup>
</section>
);
}

View File

@@ -1,8 +1,30 @@
.right-container {
display: flex;
flex-direction: column;
font-family: 'Space Mono';
padding-bottom: 48px;
.section-heading {
color: var(--bg-vanilla-400);
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.section-heading-small {
font-family: 'Space Mono';
color: var(--bg-vanilla-400);
font-size: 12px;
font-style: normal;
font-weight: 400;
word-break: initial;
line-height: 16px; /* 133.333% */
letter-spacing: 0.48px;
}
.header {
display: flex;
padding: 14px 14px 14px 12px;
@@ -34,17 +56,6 @@
.name-description {
padding: 0 0 4px 0;
.typography {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.name-input {
display: flex;
padding: 6px 6px 6px 8px;
@@ -90,15 +101,21 @@
display: flex;
flex-direction: column;
.typography {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
.toggle-card {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
}
.toggle-card-text-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.panel-type-select {
@@ -114,55 +131,16 @@
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
.select-option {
display: flex;
align-items: center;
gap: 6px;
.icon {
display: flex;
align-items: center;
}
.display {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
}
}
}
.fill-gaps {
display: flex;
padding: 12px;
justify-content: space-between;
align-items: center;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.fill-gaps-text {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.fill-gaps-text-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
}
.toggle-card-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
}
.log-scale,
@@ -171,17 +149,6 @@
justify-content: space-between;
}
.panel-time-text {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.y-axis-unit-selector,
.y-axis-unit-selector-v2 {
display: flex;
@@ -189,14 +156,7 @@
gap: 8px;
.heading {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
@extend .section-heading;
}
.input {
@@ -249,7 +209,6 @@
.text {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
@@ -270,30 +229,9 @@
.stack-chart {
flex-direction: row;
justify-content: space-between;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
}
.bucket-config {
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.bucket-size-label {
margin-top: 8px;
}
@@ -322,7 +260,6 @@
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
@@ -354,7 +291,6 @@
.alerts-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
@@ -400,6 +336,9 @@
.lightMode {
.right-container {
background-color: var(--bg-vanilla-100);
.section-heading {
color: var(--bg-ink-400);
}
.header {
.header-text {
color: var(--bg-ink-400);
@@ -427,10 +366,6 @@
}
.panel-config {
.typography {
color: var(--bg-ink-400);
}
.panel-type-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
@@ -455,14 +390,14 @@
}
}
.fill-gaps {
.toggle-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
.fill-gaps-text {
color: var(--bg-ink-400);
}
.fill-gaps-text-description {
.toggle-card-description {
color: var(--bg-ink-400);
}
}

View File

@@ -6,6 +6,11 @@ import { MemoryRouter } from 'react-router-dom';
import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
@@ -165,6 +170,14 @@ describe('RightContainer - Alerts Section', () => {
setContextLinks: jest.fn(),
enableDrillDown: false,
isNewDashboard: false,
lineInterpolation: LineInterpolation.Spline,
fillMode: FillMode.None,
lineStyle: LineStyle.Solid,
setLineInterpolation: jest.fn(),
setFillMode: jest.fn(),
setLineStyle: jest.fn(),
showPoints: false,
setShowPoints: jest.fn(),
};
beforeEach(() => {

View File

@@ -43,7 +43,7 @@
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
transition: max-height 0.1s ease, opacity 0.1s ease, padding 0.1s ease;
&.open {
padding-bottom: 24px;

View File

@@ -206,3 +206,59 @@ export const panelTypeVsDecimalPrecision: {
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsLineInterpolation: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsLineStyle: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsFillMode: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsShowPoints: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;

View File

@@ -27,6 +27,11 @@ import {
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import {
Antenna,
Axis3D,
@@ -35,6 +40,7 @@ import {
LayoutDashboard,
LineChart,
Link,
Paintbrush,
Pencil,
Plus,
SlidersHorizontal,
@@ -60,11 +66,15 @@ import {
panelTypeVsContextLinks,
panelTypeVsCreateAlert,
panelTypeVsDecimalPrecision,
panelTypeVsFillMode,
panelTypeVsFillSpan,
panelTypeVsLegendColors,
panelTypeVsLegendPosition,
panelTypeVsLineInterpolation,
panelTypeVsLineStyle,
panelTypeVsLogScale,
panelTypeVsPanelTimePreferences,
panelTypeVsShowPoints,
panelTypeVsSoftMinMax,
panelTypeVsStackingChartPreferences,
panelTypeVsThreshold,
@@ -72,7 +82,10 @@ import {
} from './constants';
import ContextLinks from './ContextLinks';
import DashboardYAxisUnitSelectorWrapper from './DashboardYAxisUnitSelectorWrapper';
import { FillModeSelector } from './FillModeSelector';
import LegendColors from './LegendColors/LegendColors';
import { LineInterpolationSelector } from './LineInterpolationSelector';
import { LineStyleSelector } from './LineStyleSelector';
import ThresholdSelector from './Threshold/ThresholdSelector';
import { ThresholdProps } from './Threshold/types';
import { timePreferance } from './timeItems';
@@ -99,6 +112,14 @@ function RightContainer({
setTitle,
title,
selectedGraph,
lineInterpolation,
setLineInterpolation,
fillMode,
setFillMode,
lineStyle,
setLineStyle,
showPoints,
setShowPoints,
bucketCount,
bucketWidth,
stackedBarChart,
@@ -175,6 +196,11 @@ function RightContainer({
panelTypeVsContextLinks[selectedGraph] && enableDrillDown;
const allowDecimalPrecision = panelTypeVsDecimalPrecision[selectedGraph];
const allowLineInterpolation = panelTypeVsLineInterpolation[selectedGraph];
const allowLineStyle = panelTypeVsLineStyle[selectedGraph];
const allowFillMode = panelTypeVsFillMode[selectedGraph];
const allowShowPoints = panelTypeVsShowPoints[selectedGraph];
const { currentQuery } = useQueryBuilder();
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(PanelTypesWithData);
@@ -201,6 +227,22 @@ function RightContainer({
[allowLegendPosition, allowLegendColors],
);
const isChartAppearanceSectionVisible = useMemo(
() =>
/**
* Disabled for now as we are not done with other settings in chart appearance section
* TODO: @ahrefabhi Enable this after we are done other settings in chart appearance section
*/
// eslint-disable-next-line sonarjs/no-redundant-boolean
false &&
(allowFillMode ||
allowLineStyle ||
allowLineInterpolation ||
allowShowPoints),
[allowFillMode, allowLineStyle, allowLineInterpolation, allowShowPoints],
);
const updateCursorAndDropdown = (value: string, pos: number): void => {
setCursorPos(pos);
const lastDollar = value.lastIndexOf('$', pos - 1);
@@ -300,7 +342,7 @@ function RightContainer({
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
<section className="name-description control-container">
<Typography.Text className="typography">Name</Typography.Text>
<Typography.Text className="section-heading">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
@@ -320,7 +362,7 @@ function RightContainer({
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<Typography.Text className="section-heading">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
@@ -341,7 +383,7 @@ function RightContainer({
icon={<LayoutDashboard size={14} />}
>
<section className="panel-type control-container">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Typography.Text className="section-heading">Panel Type</Typography.Text>
<Select
onChange={setGraphHandler}
value={selectedGraph}
@@ -362,7 +404,7 @@ function RightContainer({
{allowPanelTimePreference && (
<section className="panel-time-preference control-container">
<Typography.Text className="panel-time-text">
<Typography.Text className="section-heading">
Panel Time Preference
</Typography.Text>
<TimePreference
@@ -376,7 +418,9 @@ function RightContainer({
{allowStackingBarChart && (
<section className="stack-chart control-container">
<Typography.Text className="label">Stack series</Typography.Text>
<Typography.Text className="section-heading">
Stack series
</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
@@ -386,10 +430,10 @@ function RightContainer({
)}
{allowFillSpans && (
<section className="fill-gaps">
<div className="fill-gaps-text-container">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Typography.Text className="fill-gaps-text-description">
<section className="fill-gaps toggle-card">
<div className="toggle-card-text-container">
<Typography className="section-heading">Fill gaps</Typography>
<Typography.Text className="toggle-card-description">
Fill gaps in data with 0 for continuity
</Typography.Text>
</div>
@@ -448,6 +492,36 @@ function RightContainer({
</SettingsSection>
)}
{isChartAppearanceSectionVisible && (
<SettingsSection title="Chart Appearance" icon={<Paintbrush size={14} />}>
{allowFillMode && (
<FillModeSelector value={fillMode} onChange={setFillMode} />
)}
{allowLineStyle && (
<LineStyleSelector value={lineStyle} onChange={setLineStyle} />
)}
{allowLineInterpolation && (
<LineInterpolationSelector
value={lineInterpolation}
onChange={setLineInterpolation}
/>
)}
{allowShowPoints && (
<section className="show-points toggle-card">
<div className="toggle-card-text-container">
<Typography.Text className="section-heading">
Show points
</Typography.Text>
<Typography.Text className="toggle-card-description">
Display individual data points on the chart
</Typography.Text>
</div>
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
</section>
)}
</SettingsSection>
)}
{isAxisSectionVisible && (
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
{allowSoftMinMax && (
@@ -475,7 +549,9 @@ function RightContainer({
{allowLogScale && (
<section className="log-scale control-container">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Typography.Text className="section-heading">
Y Axis Scale
</Typography.Text>
<Select
onChange={(value): void =>
setIsLogScale(value === LogScale.LOGARITHMIC)
@@ -511,7 +587,7 @@ function RightContainer({
<SettingsSection title="Legend" icon={<Layers size={14} />}>
{allowLegendPosition && (
<section className="legend-position control-container">
<Typography.Text className="typography">Position</Typography.Text>
<Typography.Text className="section-heading">Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
@@ -548,7 +624,9 @@ function RightContainer({
{allowBucketConfig && (
<SettingsSection title="Histogram / Buckets">
<section className="bucket-config control-container">
<Typography.Text className="label">Number of buckets</Typography.Text>
<Typography.Text className="section-heading">
Number of buckets
</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
@@ -559,7 +637,7 @@ function RightContainer({
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
<Typography.Text className="section-heading bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
@@ -575,7 +653,7 @@ function RightContainer({
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
<Typography.Text className="section-heading">
Merge all series into one
</Typography.Text>
<Switch
@@ -683,6 +761,14 @@ export interface RightContainerProps {
setContextLinks: Dispatch<SetStateAction<ContextLinksData>>;
enableDrillDown?: boolean;
isNewDashboard: boolean;
lineInterpolation: LineInterpolation;
setLineInterpolation: Dispatch<SetStateAction<LineInterpolation>>;
fillMode: FillMode;
setFillMode: Dispatch<SetStateAction<FillMode>>;
lineStyle: LineStyle;
setLineStyle: Dispatch<SetStateAction<LineStyle>>;
showPoints: boolean;
setShowPoints: Dispatch<SetStateAction<boolean>>;
}
RightContainer.defaultProps = {

View File

@@ -6,6 +6,11 @@ import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { WarningOutlined } from '@ant-design/icons';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@signozhq/resizable';
import { Button, Flex, Modal, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
@@ -24,12 +29,16 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import {
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
@@ -63,12 +72,7 @@ import QueryTypeTag from './LeftContainer/QueryTypeTag';
import RightContainer from './RightContainer';
import { ThresholdProps } from './RightContainer/Threshold/types';
import TimeItems, { timePreferance } from './RightContainer/timeItems';
import {
Container,
LeftContainerWrapper,
PanelContainer,
RightContainerWrapper,
} from './styles';
import { Container, PanelContainer } from './styles';
import { NewWidgetProps } from './types';
import {
getDefaultWidgetData,
@@ -204,6 +208,18 @@ function NewWidget({
const [legendPosition, setLegendPosition] = useState<LegendPosition>(
selectedWidget?.legendPosition || LegendPosition.BOTTOM,
);
const [lineInterpolation, setLineInterpolation] = useState<LineInterpolation>(
selectedWidget?.lineInterpolation || LineInterpolation.Spline,
);
const [fillMode, setFillMode] = useState<FillMode>(
selectedWidget?.fillMode || FillMode.None,
);
const [lineStyle, setLineStyle] = useState<LineStyle>(
selectedWidget?.lineStyle || LineStyle.Solid,
);
const [showPoints, setShowPoints] = useState<boolean>(
selectedWidget?.showPoints ?? false,
);
const [customLegendColors, setCustomLegendColors] = useState<
Record<string, string>
>(selectedWidget?.customLegendColors || {});
@@ -269,6 +285,10 @@ function NewWidget({
softMin,
softMax,
fillSpans: isFillSpans,
lineInterpolation,
fillMode,
lineStyle,
showPoints,
columnUnits,
bucketCount,
stackedBarChart,
@@ -304,6 +324,10 @@ function NewWidget({
stackedBarChart,
isLogScale,
legendPosition,
lineInterpolation,
fillMode,
lineStyle,
showPoints,
customLegendColors,
contextLinks,
selectedWidget.columnWidths,
@@ -757,7 +781,7 @@ function NewWidget({
}, [query, safeNavigate, dashboardId, currentQuery]);
return (
<Container>
<Container className="new-widget-container">
<div className="edit-header">
<div className="left-header">
<X
@@ -811,79 +835,102 @@ function NewWidget({
</div>
<PanelContainer>
<LeftContainerWrapper isDarkMode={useIsDarkMode()}>
<OverlayScrollbar>
{selectedWidget && (
<LeftContainer
selectedGraph={graphType}
selectedLogFields={selectedLogFields}
setSelectedLogFields={setSelectedLogFields}
selectedTracesFields={selectedTracesFields}
setSelectedTracesFields={setSelectedTracesFields}
selectedWidget={selectedWidget}
selectedTime={selectedTime}
requestData={requestData}
setRequestData={setRequestData}
isLoadingPanelData={isLoadingPanelData}
setQueryResponse={setQueryResponse}
enableDrillDown={enableDrillDown}
selectedDashboard={selectedDashboard}
isNewPanel={isNewPanel}
/>
)}
</OverlayScrollbar>
</LeftContainerWrapper>
<RightContainerWrapper>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
</RightContainerWrapper>
<ResizablePanelGroup
direction="horizontal"
className="widget-resizable-panel-group"
autoSaveId="panel-editor"
>
<ResizablePanel
minSize={70}
maxSize={80}
defaultSize={80}
className="resizable-panel-left-container"
>
<OverlayScrollbar>
{selectedWidget && (
<LeftContainer
selectedDashboard={selectedDashboard}
selectedGraph={graphType}
selectedLogFields={selectedLogFields}
setSelectedLogFields={setSelectedLogFields}
selectedTracesFields={selectedTracesFields}
setSelectedTracesFields={setSelectedTracesFields}
selectedWidget={selectedWidget}
selectedTime={selectedTime}
requestData={requestData}
setRequestData={setRequestData}
isLoadingPanelData={isLoadingPanelData}
setQueryResponse={setQueryResponse}
enableDrillDown={enableDrillDown}
/>
)}
</OverlayScrollbar>
</ResizablePanel>
<ResizableHandle withHandle className="widget-resizable-handle" />
<ResizablePanel
minSize={20}
maxSize={30}
defaultSize={20}
className="resizable-panel-right-container"
>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
lineInterpolation={lineInterpolation}
setLineInterpolation={setLineInterpolation}
fillMode={fillMode}
setFillMode={setFillMode}
lineStyle={lineStyle}
setLineStyle={setLineStyle}
showPoints={showPoints}
setShowPoints={setShowPoints}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
</ResizablePanel>
</ResizablePanelGroup>
</PanelContainer>
<Modal
title={

View File

@@ -1,4 +1,4 @@
import { Col, Tag as AntDTag } from 'antd';
import { Tag as AntDTag } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
@@ -8,42 +8,6 @@ export const Container = styled.div`
overflow-y: hidden;
`;
export const RightContainerWrapper = styled(Col)`
&&& {
max-width: 400px;
width: 30%;
overflow-y: auto;
}
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
`;
interface LeftContainerWrapperProps {
isDarkMode: boolean;
}
export const LeftContainerWrapper = styled(Col)<LeftContainerWrapperProps>`
&&& {
width: 100%;
overflow-y: auto;
border-right: ${({ isDarkMode }): string =>
isDarkMode
? '1px solid var(--bg-slate-300)'
: '1px solid var(--bg-vanilla-300)'};
}
&::-webkit-scrollbar {
width: 0rem;
}
`;
export const ButtonContainer = styled.div`
display: flex;
gap: 8px;

View File

@@ -1,4 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
/* 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 { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useEffectOnce } from 'react-use';
@@ -12,9 +14,11 @@ import ROUTES from 'constants/routes';
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
import { InviteMemberFormValues } from 'container/OrganizationSettings/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { UserPlus } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { isModifierKeyPressed } from 'utils/navigation';
import ModuleStepsContainer from './common/ModuleStepsContainer/ModuleStepsContainer';
import { stepsMap } from './constants/stepsConfig';
@@ -105,6 +109,7 @@ export default function Onboarding(): JSX.Element {
const [current, setCurrent] = useState(0);
const { location } = history;
const { t } = useTranslation(['onboarding']);
const { safeNavigate } = useSafeNavigate();
const { featureFlags } = useAppContext();
const isOnboardingV3Enabled = featureFlags?.find(
@@ -250,9 +255,11 @@ export default function Onboarding(): JSX.Element {
}
};
const handleNext = (): void => {
const handleNext = (e?: React.MouseEvent): void => {
if (activeStep <= 3) {
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
safeNavigate(moduleRouteMap[selectedModule.id as ModulesMap], {
newTab: !!e && isModifierKeyPressed(e),
});
}
};
@@ -315,9 +322,9 @@ export default function Onboarding(): JSX.Element {
{activeStep === 1 && (
<div className="onboarding-page">
<div
onClick={(): void => {
onClick={(e): void => {
logEvent('Onboarding V2: Skip Button Clicked', {});
history.push(ROUTES.APPLICATION);
safeNavigate(ROUTES.APPLICATION, { newTab: isModifierKeyPressed(e) });
}}
className="skip-to-console"
>
@@ -353,7 +360,11 @@ export default function Onboarding(): JSX.Element {
</div>
</div>
<div className="continue-to-next-step">
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
<Button
type="primary"
icon={<ArrowRightOutlined />}
onClick={(e): void => handleNext(e)}
>
{t('get_started')}
</Button>
</div>
@@ -384,17 +395,16 @@ export default function Onboarding(): JSX.Element {
{activeStep > 1 && (
<div className="stepsContainer">
<ModuleStepsContainer
onReselectModule={(): void => {
onReselectModule={(e?: React.MouseEvent): void => {
setCurrent(current - 1);
setActiveStep(activeStep - 1);
setSelectedModule(useCases.APM);
resetProgress();
if (isOnboardingV3Enabled) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
history.push(ROUTES.GET_STARTED);
}
const path = isOnboardingV3Enabled
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
safeNavigate(path, { newTab: !!e && isModifierKeyPressed(e) });
}}
selectedModule={selectedModule}
selectedModuleSteps={selectedModuleSteps}

View File

@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -19,8 +18,10 @@ import {
messagingQueueKakfaSupportedDataSources,
} from 'container/OnboardingContainer/utils/dataSourceUtils';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react';
import { isModifierKeyPressed } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import './DataSource.styles.scss';
@@ -35,7 +36,7 @@ export interface DataSourceType {
export default function DataSource(): JSX.Element {
const [form] = Form.useForm();
const { t } = useTranslation(['common']);
const history = useHistory();
const { safeNavigate } = useSafeNavigate();
const getStartedSource = useUrlQuery().get(QueryParams.getStartedSource);
@@ -139,13 +140,13 @@ export default function DataSource(): JSX.Element {
}
};
const goToIntegrationsPage = (): void => {
const goToIntegrationsPage = (e?: React.MouseEvent): void => {
logEvent('Onboarding V2: Go to integrations', {
module: selectedModule?.id,
dataSource: selectedDataSource?.name,
framework: selectedFramework,
});
history.push(ROUTES.INTEGRATIONS);
safeNavigate(ROUTES.INTEGRATIONS, { newTab: !!e && isModifierKeyPressed(e) });
};
return (
@@ -247,7 +248,7 @@ export default function DataSource(): JSX.Element {
page which allows more sources of sending data
</Typography.Text>
<Button
onClick={goToIntegrationsPage}
onClick={(e): void => goToIntegrationsPage(e)}
icon={<Blocks size={14} />}
className="navigate-integrations-page-btn"
>

View File

@@ -1,4 +1,4 @@
import { SetStateAction, useState } from 'react';
import React, { SetStateAction, useState } from 'react';
import {
ArrowLeftOutlined,
ArrowRightOutlined,
@@ -12,9 +12,10 @@ import ROUTES from 'constants/routes';
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
import history from 'lib/history';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEmpty, isNull } from 'lodash-es';
import { UserPlus } from 'lucide-react';
import { isModifierKeyPressed } from 'utils/navigation';
import { useOnboardingContext } from '../../context/OnboardingContext';
import {
@@ -63,6 +64,7 @@ export default function ModuleStepsContainer({
selectedModuleSteps,
setIsInviteTeamMemberModalOpen,
}: ModuleStepsContainerProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const {
activeStep,
serviceName,
@@ -130,7 +132,7 @@ export default function ModuleStepsContainer({
);
};
const redirectToModules = (): void => {
const redirectToModules = (event?: React.MouseEvent): void => {
logEvent('Onboarding V2 Complete', {
module: selectedModule.id,
dataSource: selectedDataSource?.id,
@@ -140,26 +142,28 @@ export default function ModuleStepsContainer({
serviceName,
});
let targetPath: string;
if (selectedModule.id === ModulesMap.APM) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else if (selectedModule.id === ModulesMap.LogsManagement) {
history.push(ROUTES.LOGS_EXPLORER);
targetPath = ROUTES.LOGS_EXPLORER;
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else if (selectedModule.id === ModulesMap.AwsMonitoring) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
}
safeNavigate(targetPath, { newTab: !!event && isModifierKeyPressed(event) });
};
const handleNext = (): void => {
const handleNext = (event?: React.MouseEvent): void => {
const isValid = isValidForm();
if (isValid) {
if (current === lastStepIndex) {
resetProgress();
redirectToModules();
redirectToModules(event);
return;
}
@@ -367,8 +371,8 @@ export default function ModuleStepsContainer({
}
};
const handleLogoClick = (): void => {
history.push('/home');
const handleLogoClick = (e: React.MouseEvent): void => {
safeNavigate('/home', { newTab: isModifierKeyPressed(e) });
};
return (
@@ -388,7 +392,7 @@ export default function ModuleStepsContainer({
style={{ display: 'flex', alignItems: 'center' }}
type="default"
icon={<LeftCircleOutlined />}
onClick={onReselectModule}
onClick={(e): void => onReselectModule(e)}
>
{selectedModule.title}
</Button>
@@ -458,7 +462,11 @@ export default function ModuleStepsContainer({
>
Back
</Button>
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
<Button
onClick={(e): void => handleNext(e)}
type="primary"
icon={<ArrowRightOutlined />}
>
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
</Button>
<LaunchChatSupport

View File

@@ -17,10 +17,11 @@ import { DOCS_BASE_URL } from 'constants/app';
import ROUTES from 'constants/routes';
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import history from 'lib/history';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEmpty } from 'lodash-es';
import { CheckIcon, Goal, UserPlus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { isModifierKeyPressed } from 'utils/navigation';
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
import InviteTeamMembers from '../InviteTeamMembers/InviteTeamMembers';
@@ -143,6 +144,7 @@ const allGroupedDataSources = groupDataSourcesByTags(
// eslint-disable-next-line sonarjs/cognitive-complexity
function OnboardingAddDataSource(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [groupedDataSources, setGroupedDataSources] = useState<{
[tag: string]: Entity[];
}>(allGroupedDataSources);
@@ -413,7 +415,10 @@ function OnboardingAddDataSource(): JSX.Element {
]);
}, [org]);
const handleUpdateCurrentStep = (step: number): void => {
const handleUpdateCurrentStep = (
step: number,
event?: React.MouseEvent,
): void => {
setCurrentStep(step);
if (step === 1) {
@@ -443,43 +448,45 @@ function OnboardingAddDataSource(): JSX.Element {
...setupStepItemsBase.slice(2),
]);
} else if (step === 3) {
let targetPath: string;
switch (selectedDataSource?.module) {
case 'apm':
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
break;
case 'logs':
history.push(ROUTES.LOGS);
targetPath = ROUTES.LOGS;
break;
case 'metrics':
history.push(ROUTES.METRICS_EXPLORER);
targetPath = ROUTES.METRICS_EXPLORER;
break;
case 'dashboards':
history.push(ROUTES.ALL_DASHBOARD);
targetPath = ROUTES.ALL_DASHBOARD;
break;
case 'infra-monitoring-hosts':
history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS);
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS;
break;
case 'infra-monitoring-k8s':
history.push(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES);
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES;
break;
case 'messaging-queues-kafka':
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
targetPath = ROUTES.MESSAGING_QUEUES_KAFKA;
break;
case 'messaging-queues-celery':
history.push(ROUTES.MESSAGING_QUEUES_CELERY_TASK);
targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK;
break;
case 'integrations':
history.push(ROUTES.INTEGRATIONS);
targetPath = ROUTES.INTEGRATIONS;
break;
case 'home':
history.push(ROUTES.HOME);
targetPath = ROUTES.HOME;
break;
case 'api-monitoring':
history.push(ROUTES.API_MONITORING);
targetPath = ROUTES.API_MONITORING;
break;
default:
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
}
safeNavigate(targetPath, { newTab: !!event && isModifierKeyPressed(event) });
}
};
@@ -633,7 +640,7 @@ function OnboardingAddDataSource(): JSX.Element {
<X
size={14}
className="onboarding-header-container-close-icon"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
{
@@ -641,7 +648,7 @@ function OnboardingAddDataSource(): JSX.Element {
},
);
history.push(ROUTES.HOME);
safeNavigate(ROUTES.HOME, { newTab: isModifierKeyPressed(e) });
}}
/>
<Typography.Text>{progressText}</Typography.Text>
@@ -968,7 +975,7 @@ function OnboardingAddDataSource(): JSX.Element {
type="primary"
disabled={!selectedDataSource}
shape="round"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
{
@@ -982,7 +989,9 @@ function OnboardingAddDataSource(): JSX.Element {
selectedEnvironment || selectedFramework || selectedDataSource;
if (currentEntity?.internalRedirect && currentEntity?.link) {
history.push(currentEntity.link);
safeNavigate(currentEntity.link, {
newTab: isModifierKeyPressed(e),
});
} else {
handleUpdateCurrentStep(2);
}
@@ -1053,7 +1062,7 @@ function OnboardingAddDataSource(): JSX.Element {
<Button
type="primary"
shape="round"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
{
@@ -1065,7 +1074,7 @@ function OnboardingAddDataSource(): JSX.Element {
);
handleFilterByCategory('All');
handleUpdateCurrentStep(3);
handleUpdateCurrentStep(3, e);
}}
>
Continue

View File

@@ -63,6 +63,7 @@ import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { useCmdK } from '../../providers/cmdKProvider';
import { routeConfig } from './config';
@@ -305,8 +306,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
icon: <Cog size={16} />,
};
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
const [
@@ -435,10 +434,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
const onClickGetStarted = (event: MouseEvent): void => {
logEvent('Sidebar: Menu clicked', {
menuRoute: '/get-started',
@@ -449,7 +444,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
if (isCtrlMetaKey(event)) {
if (isModifierKeyPressed(event)) {
openInNewTab(onboaringRoute);
} else {
history.push(onboaringRoute);
@@ -464,7 +459,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlMetaKey(event)) {
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {
@@ -627,7 +622,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
if (item.key === ROUTES.SETTINGS) {
if (isCtrlMetaKey(event)) {
if (isModifierKeyPressed(event)) {
openInNewTab(settingsRoute);
} else {
history.push(settingsRoute);
@@ -805,6 +800,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleHelpSupportMenuItemClick = (info: SidebarItem): void => {
const item = helpSupportDropdownMenuItems.find(
(item) => !('type' in item) && item.key === info.key,
@@ -814,6 +810,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
window.open(item.url, '_blank');
}
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
if (item && !('type' in item)) {
logEvent('Help Popover: Item clicked', {
menuRoute: item.key,
@@ -821,8 +819,19 @@ 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);
}
break;
case 'invite-collaborators':
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
} else {
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
}
break;
case 'chat-support':
if (window.pylon) {
@@ -839,6 +848,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleSettingsMenuItemClick = (info: SidebarItem): void => {
const item = (userSettingsDropdownMenuItems ?? []).find(
(item) => item?.key === info.key,
@@ -856,18 +866,37 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
menuRoute: item?.key,
menuLabel,
});
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
switch (info.key) {
case 'account':
history.push(ROUTES.MY_SETTINGS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.MY_SETTINGS);
} else {
history.push(ROUTES.MY_SETTINGS);
}
break;
case 'workspace':
history.push(ROUTES.SETTINGS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SETTINGS);
} else {
history.push(ROUTES.SETTINGS);
}
break;
case 'license':
history.push(ROUTES.LIST_LICENSES);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.LIST_LICENSES);
} else {
history.push(ROUTES.LIST_LICENSES);
}
break;
case 'keyboard-shortcuts':
history.push(ROUTES.SHORTCUTS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SHORTCUTS);
} else {
history.push(ROUTES.SHORTCUTS);
}
break;
case 'logout':
Logout();

View File

@@ -0,0 +1,80 @@
/**
* 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 newTab option 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';
describe('useSafeNavigate mock contract', () => {
it('mock returns a safeNavigate function', () => {
const { safeNavigate } = useSafeNavigate();
expect(typeof safeNavigate).toBe('function');
});
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 string path without options', () => {
const { safeNavigate } = useSafeNavigate();
expect(() => {
safeNavigate('/alerts');
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith('/alerts');
});
it('safeNavigate accepts SafeNavigateParams with newTab option', () => {
const { safeNavigate } = useSafeNavigate();
expect(() => {
safeNavigate(
{ pathname: '/settings', search: '?tab=general' },
{ newTab: false },
);
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith(
{ pathname: '/settings', search: '?tab=general' },
{ newTab: false },
);
});
});
describe('shouldOpenNewTab decision logic', () => {
it('returns true when newTab is true', () => {
expect(true).toBe(true);
});
it('returns false when newTab is false', () => {
expect(false).toBe(false);
});
it('returns false when no options provided', () => {
const shouldOpenNewTab = (options?: { newTab?: boolean }): boolean =>
Boolean(options?.newTab);
expect(shouldOpenNewTab()).toBe(false);
expect(shouldOpenNewTab(undefined)).toBe(false);
});
it('returns false when options provided without newTab', () => {
const shouldOpenNewTab = (options?: { newTab?: boolean }): boolean =>
Boolean(options?.newTab);
expect(shouldOpenNewTab({})).toBe(false);
});
});

View File

@@ -105,6 +105,7 @@ export const useSafeNavigate = (
const location = useLocation();
const safeNavigate = useCallback(
// eslint-disable-next-line sonarjs/cognitive-complexity
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
const currentUrl = new URL(
`${location.pathname}${location.search}`,
@@ -122,8 +123,9 @@ export const useSafeNavigate = (
);
}
// If newTab is true, open in new tab and return early
if (options?.newTab) {
const shouldOpenNewTab = options?.newTab;
if (shouldOpenNewTab) {
const targetPath =
typeof to === 'string'
? to

View File

@@ -3,14 +3,15 @@ import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { calculateWidthBasedOnStepInterval } from 'lib/uPlotV2/utils';
import uPlot, { Series } from 'uplot';
import { generateGradientFill } from '../utils/generateGradientFill';
import {
BarAlignment,
ConfigBuilder,
DrawStyle,
FillMode,
LineInterpolation,
LineStyle,
SeriesProps,
VisibilityMode,
} from './types';
/**
@@ -52,7 +53,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
}: {
resolvedLineColor: string;
}): Partial<Series> {
const { lineWidth, lineStyle, lineCap, fillColor } = this.props;
const { lineWidth, lineStyle, lineCap, fillColor, fillMode } = this.props;
const lineConfig: Partial<Series> = {
stroke: resolvedLineColor,
width: lineWidth ?? DEFAULT_LINE_WIDTH,
@@ -66,12 +67,26 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
lineConfig.cap = lineCap;
}
if (fillColor) {
lineConfig.fill = fillColor;
} else if (this.props.drawStyle === DrawStyle.Bar) {
lineConfig.fill = resolvedLineColor;
/**
* Configure area fill based on draw style and fill mode:
* - bar charts always use a solid fill with the series color
* - histogram uses the same color with a fixed alpha suffix for translucency
* - for other series, an explicit fillMode controls whether we use a solid fill
* or a vertical gradient from the series color to transparent
*/
const finalFillColor = fillColor ?? resolvedLineColor;
if (this.props.drawStyle === DrawStyle.Bar) {
lineConfig.fill = finalFillColor;
} else if (this.props.drawStyle === DrawStyle.Histogram) {
lineConfig.fill = `${resolvedLineColor}40`;
lineConfig.fill = `${finalFillColor}40`;
} else if (fillMode && fillMode !== FillMode.None) {
if (fillMode === FillMode.Solid) {
lineConfig.fill = finalFillColor;
} else if (fillMode === FillMode.Gradient) {
lineConfig.fill = (self: uPlot): CanvasGradient =>
generateGradientFill(self, finalFillColor, 'rgba(0, 0, 0, 0)');
}
}
return lineConfig;
@@ -159,12 +174,8 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
pointsConfig.show = pointsBuilder;
} else if (drawStyle === DrawStyle.Points) {
pointsConfig.show = true;
} else if (showPoints === VisibilityMode.Never) {
pointsConfig.show = false;
} else if (showPoints === VisibilityMode.Always) {
pointsConfig.show = true;
} else {
pointsConfig.show = false; // default to hidden
pointsConfig.show = !!showPoints;
}
return pointsConfig;

View File

@@ -2,12 +2,7 @@ import { themeColors } from 'constants/theme';
import uPlot from 'uplot';
import type { SeriesProps } from '../types';
import {
DrawStyle,
LineInterpolation,
LineStyle,
VisibilityMode,
} from '../types';
import { DrawStyle, LineInterpolation, LineStyle } from '../types';
import { POINT_SIZE_FACTOR, UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
const createBaseProps = (
@@ -168,17 +163,17 @@ describe('UPlotSeriesBuilder', () => {
expect(config.points?.show).toBe(pointsBuilder);
});
it('respects VisibilityMode for point visibility when no custom pointsBuilder is given', () => {
it('respects showPoints for point visibility when no custom pointsBuilder is given', () => {
const neverPointsBuilder = new UPlotSeriesBuilder(
createBaseProps({
drawStyle: DrawStyle.Line,
showPoints: VisibilityMode.Never,
showPoints: false,
}),
);
const alwaysPointsBuilder = new UPlotSeriesBuilder(
createBaseProps({
drawStyle: DrawStyle.Line,
showPoints: VisibilityMode.Always,
showPoints: true,
}),
);

View File

@@ -122,12 +122,6 @@ export enum LineInterpolation {
StepBefore = 'stepBefore',
}
export enum VisibilityMode {
Always = 'always',
Auto = 'auto',
Never = 'never',
}
/**
* Props for configuring lines
*/
@@ -163,7 +157,13 @@ export interface BarConfig {
export interface PointsConfig {
pointColor?: string;
pointSize?: number;
showPoints?: VisibilityMode;
showPoints?: boolean;
}
export enum FillMode {
Solid = 'solid',
Gradient = 'gradient',
None = 'none',
}
export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
@@ -177,6 +177,7 @@ export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
show?: boolean;
spanGaps?: boolean;
fillColor?: string;
fillMode?: FillMode;
isDarkMode?: boolean;
stepInterval?: number;
}

View File

@@ -0,0 +1,18 @@
import uPlot from 'uplot';
export function generateGradientFill(
uPlotInstance: uPlot,
startColor: string,
endColor: string,
): CanvasGradient {
const g = uPlotInstance.ctx.createLinearGradient(
0,
0,
0,
uPlotInstance.bbox.height,
);
g.addColorStop(0, `${startColor}70`);
g.addColorStop(0.6, `${startColor}40`);
g.addColorStop(1, endColor);
return g;
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Breadcrumb, Button, Divider } from 'antd';
@@ -11,6 +11,7 @@ import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -18,6 +19,7 @@ import {
NEW_ALERT_SCHEMA_VERSION,
PostableAlertRuleV2,
} from 'types/api/alerts/alertTypesV2';
import { isModifierKeyPressed } from 'utils/navigation';
import AlertHeader from './AlertHeader/AlertHeader';
import AlertNotFound from './AlertNotFound';
@@ -55,14 +57,15 @@ function BreadCrumbItem({
isLast?: boolean;
route?: string;
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
if (isLast) {
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
}
const handleNavigate = (): void => {
const handleNavigate = (e: React.MouseEvent): void => {
if (!route) {
return;
}
history.push(ROUTES.LIST_ALL_ALERT);
safeNavigate(ROUTES.LIST_ALL_ALERT, { newTab: isModifierKeyPressed(e) });
};
return (

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { Button, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { LifeBuoy, List } from 'lucide-react';
import { handleContactSupport } from 'pages/Integrations/utils';
import { isModifierKeyPressed } from 'utils/navigation';
import './AlertNotFound.styles.scss';
@@ -15,8 +17,8 @@ function AlertNotFound({ isTestAlert }: AlertNotFoundProps): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { safeNavigate } = useSafeNavigate();
const checkAllRulesHandler = (): void => {
safeNavigate(ROUTES.LIST_ALL_ALERT);
const checkAllRulesHandler = (e: React.MouseEvent): void => {
safeNavigate(ROUTES.LIST_ALL_ALERT, { newTab: isModifierKeyPressed(e) });
};
const contactSupportHandler = (): void => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ import {
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { formatNumericValue } from 'utils/numericUtils';
import { getTableDataForProducerLatencyOverview } from './MQTableUtils';
@@ -36,6 +37,7 @@ import './MQTables.styles.scss';
const INITIAL_PAGE_SIZE = 10;
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns(
data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>,
@@ -77,7 +79,12 @@ export function getColumns(
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
history.push(`/services/${encodeURIComponent(text)}`);
const path = `/services/${encodeURIComponent(text)}`;
if (isModifierKeyPressed(e)) {
openInNewTab(path);
} else {
history.push(path);
}
}}
>
{text}

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect } from 'react';
/* eslint-disable react/no-unescaped-entities */
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import type { TabsProps } from 'antd';
@@ -21,11 +22,13 @@ import updateCreditCardApi from 'api/v1/checkout/create';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
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 { isModifierKeyPressed } from 'utils/navigation';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
@@ -48,6 +51,7 @@ export default function WorkspaceBlocked(): JSX.Element {
} = useAppContext();
const isAdmin = user.role === 'ADMIN';
const { notifications } = useNotifications();
const { safeNavigate } = useSafeNavigate();
const { t } = useTranslation(['workspaceLocked']);
@@ -131,10 +135,10 @@ export default function WorkspaceBlocked(): JSX.Element {
});
};
const handleViewBilling = (): void => {
const handleViewBilling = (e?: React.MouseEvent): void => {
logEvent('Workspace Blocked: User Clicked View Billing', {});
history.push(ROUTES.BILLING);
safeNavigate(ROUTES.BILLING, { newTab: !!e && isModifierKeyPressed(e) });
};
const renderCustomerStories = (
@@ -294,7 +298,7 @@ export default function WorkspaceBlocked(): JSX.Element {
type="link"
size="small"
role="button"
onClick={handleViewBilling}
onClick={(e): void => handleViewBilling(e)}
>
View Billing
</Button>

View File

@@ -5,6 +5,11 @@ import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import {
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { IField } from '../logs/fields';
@@ -132,6 +137,10 @@ export interface IBaseWidget {
legendPosition?: LegendPosition;
customLegendColors?: Record<string, string>;
contextLinks?: ContextLinksData;
lineInterpolation?: LineInterpolation;
showPoints?: boolean;
lineStyle?: LineStyle;
fillMode?: FillMode;
}
export interface Widgets extends IBaseWidget {
query: Query;

View File

@@ -0,0 +1,79 @@
import { isModifierKeyPressed, 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,
button: 0,
...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);
});
it('returns true when middle mouse button is used', () => {
const event = createMouseEvent({ button: 1 });
expect(isModifierKeyPressed(event)).toBe(true);
});
});
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',
);
});
});
});

View File

@@ -0,0 +1,17 @@
import React from 'react';
/**
* Returns true if the user is holding Cmd (Mac) or Ctrl (Windows/Linux)
* during a click event, or if the middle mouse button is used —
* the universal "open in new tab" modifiers.
*/
export const isModifierKeyPressed = (
event: MouseEvent | React.MouseEvent,
): boolean => event.metaKey || event.ctrlKey || event.button === 1;
/**
* Opens the given path in a new browser tab.
*/
export const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};

View File

@@ -5646,7 +5646,7 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/toggle-group@^0.0.1":
"@signozhq/toggle-group@0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@signozhq/toggle-group/-/toggle-group-0.0.1.tgz#c82ff1da34e77b24da53c2d595ad6b4a0d1b1de4"
integrity sha512-871bQayL5MaqsuNOFHKexidu9W2Hlg1y4xmH8C5mGmlfZ4bd0ovJ9OweQrM6Puys3jeMwi69xmJuesYCfKQc1g==

View File

@@ -1,70 +0,0 @@
package common
// Signal is the telemetry signal type for a builder query.
#Signal: "metrics" | "logs" | "traces"
// QueryName is a valid identifier for a query (e.g., "A", "B1", "my_query").
#QueryName: =~"^[A-Za-z][A-Za-z0-9_]*$"
// ReduceTo specifies how a multi-series result is reduced to a single value.
#ReduceTo: "sum" | "count" | "avg" | "min" | "max" | "last" | "median"
// Limit constrains the maximum number of result rows.
#Limit: int & >=0 & <=10000
// PageSize constrains the number of rows per page.
#PageSize: int & >=1
// Offset is a non-negative row offset for pagination.
#Offset: int & >=0
// VariableSortOrder controls how variable values are sorted.
#VariableSortOrder: *"DISABLED" | "ASC" | "DESC"
// MetricAggregation defines a structured aggregation for metrics queries.
#MetricAggregation: close({
metricName: string & !=""
timeAggregation: "latest" | "sum" | "avg" | "min" | "max" | "count" | "rate" | "increase"
spaceAggregation: "sum" | "avg" | "min" | "max" | "count" | "p50" | "p75" | "p90" | "p95" | "p99"
reduceTo?: #ReduceTo
temporality?: "delta" | "cumulative" | "unspecified"
})
// ExpressionAggregation defines an expression-based aggregation for logs/traces queries.
#ExpressionAggregation: close({
expression: string & !=""
alias?: string
})
// FilterExpression is a filter condition string.
#FilterExpression: close({
expression: string
})
// GroupByItem specifies a grouping column.
#GroupByItem: close({
name: string & !=""
fieldDataType?: string
fieldContext?: string
})
// OrderByItem specifies a column ordering.
#OrderByItem: close({
columnName: string & !=""
order: "asc" | "desc"
})
// HavingExpression is a post-aggregation filter.
#HavingExpression: close({
expression: string
})
// Function is a post-query transformation.
#Function: close({
name: "cutOffMin" | "cutOffMax" | "clampMin" | "clampMax" |
"absolute" | "runningDiff" | "log2" | "log10" |
"cumulativeSum" | "ewma3" | "ewma5" | "ewma7" |
"median3" | "median5" | "median7" | "timeShift" |
"anomaly" | "fillZero"
args?: [...close({value: number | string | bool})]
})

View File

@@ -1,4 +0,0 @@
module: "github.com/signoz/signoz/schemas"
language: {
version: "v0.12.0"
}

View File

@@ -1,9 +0,0 @@
{
"id": "signoz",
"name": "signoz",
"metaData": {
"buildInfo": {
"buildVersion": "0.0.1"
}
}
}

View File

@@ -1,118 +0,0 @@
{
"name": "@signoz/perses-plugin",
"version": "0.0.1",
"_comment_panels": "TimeSeriesChart is the only panel type used currently. Other Perses built-in panels like StatChart, GaugeChart, BarChart, PieChart, Table, etc. can be added as needed.",
"_comment_queries": "Only TimeSeriesQuery is used currently since the example dashboard only has time series queries. LogQuery, TraceQuery, and ProfileQuery kinds will also be needed.",
"perses": {
"plugins": [
{
"kind": "Panel",
"spec": {
"name": "TimeSeriesChart",
"display": {
"name": "Time Series Chart"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozBuilderQuery",
"display": {
"name": "SigNoz Builder Query"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozFormula",
"display": {
"name": "SigNoz Formula"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozJoin",
"display": {
"name": "SigNoz Join"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozTraceOperator",
"display": {
"name": "SigNoz Trace Operator"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozCompositeQuery",
"display": {
"name": "SigNoz Composite Query"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozPromQL",
"display": {
"name": "SigNoz PromQL"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozClickHouseSQL",
"display": {
"name": "SigNoz ClickHouse SQL"
}
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozQueryVariable",
"display": {
"name": "SigNoz Query Variable"
}
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozCustomVariable",
"display": {
"name": "SigNoz Custom Variable"
}
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozDynamicVariable",
"display": {
"name": "SigNoz Dynamic Variable"
}
}
},
{
"kind": "Datasource",
"spec": {
"name": "SigNozDatasource",
"display": {
"name": "SigNoz Datasource"
}
}
}
]
}
}

View File

@@ -1,27 +0,0 @@
Panel Plugins
All SigNoz panels use Perses built-in panel kinds directly. No custom CUE
schemas exist yet because Perses does not publish CUE schemas for its panel
plugins (only Go SDK + TypeScript). Once it does, each panel can embed the
upstream spec and add the SigNoz-specific fields listed below.
SigNoz-specific fields by panel type:
TimeSeriesChart timePreference
StatChart timePreference, contextLinks
BarChart timePreference, contextLinks
PieChart timePreference, contextLinks
Table timePreference, contextLinks
LogsTable (List) timePreference, selectedLogFields, selectedTracesFields, columnWidths
TraceTable timePreference, selectedTracesFields, columnWidths
HistogramChart timePreference, contextLinks, bucketCount, bucketWidth, mergeAllActiveQueries
Common fields:
timePreference — panel-local vs dashboard-global time range
contextLinks — clickable drill-down links on data points
Panel-specific fields:
selectedLogFields / selectedTracesFields — which fields to display as columns in list views
columnWidths — saved column width overrides
bucketCount / bucketWidth — histogram bucket configuration
mergeAllActiveQueries — combine multiple queries into one histogram

View File

@@ -1,37 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/builder_query.go — QueryBuilderQuery
kind: "SigNozBuilderQuery"
spec: close({
name: common.#QueryName
signal: common.#Signal
expression: string
disabled?: bool | *false
// Metrics use structured aggregations; logs/traces use expression-based.
aggregations?: [...common.#MetricAggregation]
expressionAggregations?: [...common.#ExpressionAggregation]
filter?: common.#FilterExpression
groupBy?: [...common.#GroupByItem]
order?: [...common.#OrderByItem]
selectFields?: [...]
limit?: common.#Limit
limitBy?: #LimitBy
offset?: common.#Offset
cursor?: string
having?: common.#HavingExpression
// secondaryAggregations not added — not yet implemented.
functions?: [...common.#Function]
legend?: string
stepInterval?: number
reduceTo?: common.#ReduceTo
pageSize?: common.#PageSize
source?: string
})
#LimitBy: close({
keys: [...string]
value: string
})

View File

@@ -1,24 +0,0 @@
{
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"order": [],
"disabled": false,
"legend": "Hit/s across all hosts",
"stepInterval": 60
}
}

View File

@@ -1,12 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/clickhouse_query.go — ClickHouseQuery
kind: "SigNozClickHouseSQL"
spec: close({
name: common.#QueryName
query: string & !=""
disabled?: bool | *false
legend?: string
})

View File

@@ -1,9 +0,0 @@
{
"kind": "SigNozClickHouseSQL",
"spec": {
"name": "A",
"query": "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts, count() AS total FROM signoz_logs.distributed_logs GROUP BY ts ORDER BY ts",
"disabled": false,
"legend": "Log count"
}
}

View File

@@ -1,24 +0,0 @@
package model
// Source: pkg/types/querybuildertypes/querybuildertypesv5/req.go — CompositeQuery
// SigNozCompositeQuery groups multiple query plugins into a single
// query request. Each entry is a typed envelope whose spec is
// validated by the corresponding plugin schema.
kind: "SigNozCompositeQuery"
spec: close({
queries: [...#QueryEnvelope]
})
// QueryEnvelope wraps a single query plugin with a type discriminator.
#QueryEnvelope: close({
type: #QueryType
spec: {...}
})
#QueryType:
"builder_query" |
"builder_formula" |
"builder_join" |
"builder_trace_operator" |
"promql" |
"clickhouse_sql"

View File

@@ -1,53 +0,0 @@
{
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "metrics",
"expression": "B",
"aggregations": [
{
"metricName": "redis_keyspace_misses",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "A / (A + B) * 100",
"legend": "Hit rate %"
}
}
]
}
}

View File

@@ -1,12 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozCustomVariable"
spec: close({
customValue: string
sort?: common.#VariableSortOrder
multiSelect?: bool
showALLOption?: bool
})

View File

@@ -1,8 +0,0 @@
{
"kind": "SigNozCustomVariable",
"spec": {
"customValue": "production,staging,development",
"sort": "DISABLED",
"multiSelect": false
}
}

View File

@@ -1,9 +0,0 @@
package model
kind: "SigNozDatasource"
// SigNoz has a single built-in backend — the frontend already knows
// the API endpoint, so there is no connection config to validate.
// Add fields here if SigNoz ever supports multiple backends or
// configurable API versions.
spec: close({})

View File

@@ -1,4 +0,0 @@
{
"kind": "SigNozDatasource",
"spec": {}
}

View File

@@ -1,13 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozDynamicVariable"
spec: close({
dynamicVariablesAttribute: string
dynamicVariablesSource: string
sort?: common.#VariableSortOrder
multiSelect?: bool
showALLOption?: bool
})

View File

@@ -1,10 +0,0 @@
{
"kind": "SigNozDynamicVariable",
"spec": {
"dynamicVariablesAttribute": "host_name",
"dynamicVariablesSource": "metrics",
"sort": "ASC",
"multiSelect": true,
"showALLOption": true
}
}

View File

@@ -1,16 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/formula.go — QueryBuilderFormula
kind: "SigNozFormula"
spec: close({
name: common.#QueryName
expression: string
disabled?: bool | *false
legend?: string
limit?: common.#Limit
having?: common.#HavingExpression
stepInterval?: number
order?: [...common.#OrderByItem]
})

Some files were not shown because too many files have changed in this diff Show More