mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-18 23:12:36 +00:00
Compare commits
2 Commits
fix/render
...
chore/supp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
330e2d2b07 | ||
|
|
3657e8b66f |
@@ -2,6 +2,8 @@
|
||||
interface SafeNavigateOptions {
|
||||
replace?: boolean;
|
||||
state?: unknown;
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
interface SafeNavigateTo {
|
||||
@@ -20,9 +22,7 @@ interface UseSafeNavigateReturn {
|
||||
|
||||
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
|
||||
safeNavigate: jest.fn(
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => {
|
||||
console.log(`Mock safeNavigate called with:`, to, options);
|
||||
},
|
||||
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
|
||||
) as jest.MockedFunction<
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
|
||||
>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
@@ -214,7 +214,7 @@ function LogDetailInner({
|
||||
};
|
||||
|
||||
// Go to logs explorer page with the log data
|
||||
const handleOpenInExplorer = (): void => {
|
||||
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
|
||||
const queryParams = {
|
||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||
[QueryParams.startTime]: minTime?.toString() || '',
|
||||
@@ -227,7 +227,9 @@ function LogDetailInner({
|
||||
),
|
||||
),
|
||||
};
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
|
||||
event: e,
|
||||
});
|
||||
};
|
||||
|
||||
const handleQueryExpressionChange = useCallback(
|
||||
|
||||
@@ -70,8 +70,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
</Tag>
|
||||
) : undefined
|
||||
}
|
||||
onClick={(): void => {
|
||||
onSelect(option.selection);
|
||||
onClick={(e): void => {
|
||||
onSelect(option.selection, e);
|
||||
}}
|
||||
data-testid={`alert-type-card-${option.selection}`}
|
||||
>
|
||||
@@ -108,7 +108,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface SelectAlertTypeProps {
|
||||
onSelect: (typ: AlertTypes) => void;
|
||||
onSelect: (type: AlertTypes, event?: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export default SelectAlertType;
|
||||
|
||||
@@ -33,9 +33,9 @@ function Footer(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
const handleDiscard = (e: React.MouseEvent): void => {
|
||||
discardAlertRule();
|
||||
safeNavigate('/alerts');
|
||||
safeNavigate('/alerts', { event: e });
|
||||
};
|
||||
|
||||
const alertValidationMessage = useMemo(
|
||||
|
||||
@@ -161,6 +161,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
// Verify navigation was called with correct URL
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
'/dashboard?columnKey=updatedAt&order=descend&page=1&search=',
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
|
||||
// Ensure the URL contains only essential dashboard list params
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -23,16 +23,19 @@ function DashboardBreadcrumbs(): JSX.Element {
|
||||
|
||||
const { title = '', image = Base64Icons[0] } = selectedData || {};
|
||||
|
||||
const goToListPage = useCallback(() => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlParams.set('order', listSortOrder.order as string);
|
||||
urlParams.set('page', listSortOrder.pagination as string);
|
||||
urlParams.set('search', listSortOrder.search as string);
|
||||
const goToListPage = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlParams.set('order', listSortOrder.order as string);
|
||||
urlParams.set('page', listSortOrder.pagination as string);
|
||||
urlParams.set('search', listSortOrder.search as string);
|
||||
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}, [listSortOrder, safeNavigate]);
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
|
||||
safeNavigate(generatedUrl, { event: e });
|
||||
},
|
||||
[listSortOrder, safeNavigate],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dashboard-breadcrumbs">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -329,13 +329,18 @@ function FormAlertRules({
|
||||
}
|
||||
}, [alertDef, currentQuery?.queryType, queryOptions]);
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, [safeNavigate, urlQuery]);
|
||||
const onCancelHandler = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
|
||||
event: e,
|
||||
});
|
||||
},
|
||||
[safeNavigate, urlQuery],
|
||||
);
|
||||
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
LoadingOutlined,
|
||||
@@ -279,9 +285,9 @@ function FullView({
|
||||
<Button
|
||||
className="switch-edit-btn"
|
||||
disabled={response.isFetching || response.isLoading}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
if (dashboardEditView) {
|
||||
safeNavigate(dashboardEditView);
|
||||
safeNavigate(dashboardEditView, { event: e });
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Popover } from 'antd';
|
||||
@@ -28,6 +28,7 @@ import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isIngestionActive } from 'utils/app';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import AlertRules from './AlertRules/AlertRules';
|
||||
@@ -413,12 +414,12 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="active-ingestion-card-actions"
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -455,11 +456,11 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -496,11 +497,11 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
navigateToPage(ROUTES.METRICS_EXPLORER, history.push, e);
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -550,11 +551,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
|
||||
}}
|
||||
>
|
||||
Open Logs Explorer
|
||||
@@ -564,11 +565,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
|
||||
}}
|
||||
>
|
||||
Open Traces Explorer
|
||||
@@ -578,11 +579,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
|
||||
navigateToPage(ROUTES.METRICS_EXPLORER_EXPLORER, history.push, e);
|
||||
}}
|
||||
>
|
||||
Open Metrics Explorer
|
||||
@@ -619,11 +620,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
navigateToPage(ROUTES.ALL_DASHBOARD, history.push, e);
|
||||
}}
|
||||
>
|
||||
Create dashboard
|
||||
@@ -661,11 +662,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Alerts',
|
||||
});
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
navigateToPage(ROUTES.ALERTS_NEW, history.push, e);
|
||||
}}
|
||||
>
|
||||
Create an alert
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -116,7 +116,7 @@ const ServicesListTable = memo(
|
||||
onRowClick,
|
||||
}: {
|
||||
services: ServicesList[];
|
||||
onRowClick: (record: ServicesList) => void;
|
||||
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
|
||||
}): JSX.Element => (
|
||||
<div className="services-list-container home-data-item-container metrics-services-list">
|
||||
<div className="services-list">
|
||||
@@ -125,8 +125,8 @@ const ServicesListTable = memo(
|
||||
dataSource={services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => onRowClick(record),
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -284,11 +284,11 @@ function ServiceMetrics({
|
||||
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: ServicesList) => {
|
||||
(record: ServicesList, event: React.MouseEvent) => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Select, Skeleton, Table } from 'antd';
|
||||
@@ -172,13 +172,13 @@ export default function ServiceTraces({
|
||||
dataSource={top5Services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
@@ -76,7 +77,16 @@ export default function HostsListTable({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: HostRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('hostName', record.hostName);
|
||||
openInNewTab(`${window.location.pathname}?${params.toString()}`);
|
||||
return;
|
||||
}
|
||||
onHostClick(record.hostName);
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
@@ -180,8 +190,8 @@ export default function HostsListTable({
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string => 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',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,6 +24,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -451,7 +452,19 @@ function K8sClustersList({
|
||||
);
|
||||
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
|
||||
|
||||
const handleRowClick = (record: K8sClustersRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sClustersRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedClusterName(record.clusterUID);
|
||||
@@ -515,8 +528,19 @@ function K8sClustersList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
|
||||
record.clusterUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setselectedClusterName(record.clusterUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -707,8 +731,10 @@ function K8sClustersList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -457,7 +458,19 @@ function K8sDaemonSetsList({
|
||||
nestedDaemonSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sDaemonSetsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
@@ -521,8 +534,19 @@ function K8sDaemonSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
|
||||
record.daemonsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -715,8 +739,10 @@ function K8sDaemonSetsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -463,7 +464,19 @@ function K8sDeploymentsList({
|
||||
nestedDeploymentsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sDeploymentsRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sDeploymentsRowData,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
@@ -527,8 +540,19 @@ function K8sDeploymentsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
|
||||
record.deploymentUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -722,8 +746,10 @@ function K8sDeploymentsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -428,7 +429,16 @@ function K8sJobsList({
|
||||
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
|
||||
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
|
||||
|
||||
const handleRowClick = (record: K8sJobsRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sJobsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedJobUID(record.jobUID);
|
||||
@@ -492,8 +502,16 @@ function K8sJobsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setselectedJobUID(record.jobUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -684,8 +702,10 @@ function K8sJobsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,6 +24,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -459,7 +460,19 @@ function K8sNamespacesList({
|
||||
nestedNamespacesData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sNamespacesRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sNamespacesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
|
||||
record.namespaceUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
@@ -523,8 +536,19 @@ function K8sNamespacesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
|
||||
record.namespaceUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -716,8 +740,10 @@ function K8sNamespacesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -24,6 +24,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -438,7 +439,16 @@ function K8sNodesList({
|
||||
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
|
||||
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
|
||||
|
||||
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sNodesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
@@ -503,8 +513,19 @@ function K8sNodesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
|
||||
record.nodeUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -695,8 +716,10 @@ function K8sNodesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -26,6 +26,7 @@ import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -495,7 +496,16 @@ function K8sPodsList({
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const handleRowClick = (record: K8sPodsRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sPodsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedPodUID(record.podUID);
|
||||
setSearchParams({
|
||||
@@ -615,8 +625,14 @@ function K8sPodsList({
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setSelectedPodUID(record.podUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -752,8 +768,8 @@ function K8sPodsList({
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -460,7 +461,19 @@ function K8sStatefulSetsList({
|
||||
nestedStatefulSetsData,
|
||||
]);
|
||||
|
||||
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sStatefulSetsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
|
||||
record.statefulsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
@@ -524,8 +537,19 @@ function K8sStatefulSetsList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
|
||||
record.statefulsetUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -718,8 +742,8 @@ function K8sStatefulSetsList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(record) => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -25,6 +25,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
@@ -390,7 +391,16 @@ function K8sVolumesList({
|
||||
);
|
||||
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
|
||||
|
||||
const handleRowClick = (record: K8sVolumesRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: K8sVolumesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
@@ -454,8 +464,19 @@ function K8sVolumesList({
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
|
||||
record.volumeUID,
|
||||
);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
@@ -641,8 +662,10 @@ function K8sVolumesList({
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -7,6 +7,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import AlertInfoCard from './AlertInfoCard';
|
||||
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
||||
@@ -36,9 +37,13 @@ export function AlertsEmptyState(): JSX.Element {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
const onClickNewAlertHandler = useCallback((e: React.MouseEvent) => {
|
||||
setLoading(false);
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(ROUTES.ALERTS_NEW);
|
||||
} else {
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
@@ -100,16 +100,22 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
});
|
||||
}, [notificationsApi, t]);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
|
||||
const onClickNewAlertHandler = useCallback(
|
||||
(e: React.MouseEvent): void => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, { event: e });
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
[],
|
||||
);
|
||||
|
||||
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
|
||||
const onEditHandler = (
|
||||
record: GettableAlert,
|
||||
options?: { event?: React.MouseEvent; newTab?: boolean },
|
||||
): void => {
|
||||
const compositeQuery = sanitizeDefaultAlertQuery(
|
||||
mapQueryDataFromApi(record.condition.compositeQuery),
|
||||
record.alertType as AlertTypes,
|
||||
@@ -125,11 +131,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
|
||||
setEditLoader(false);
|
||||
|
||||
if (openInNewTab) {
|
||||
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
|
||||
} else {
|
||||
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}
|
||||
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, {
|
||||
event: options?.event,
|
||||
newTab: options?.newTab,
|
||||
});
|
||||
};
|
||||
|
||||
const onCloneHandler = (
|
||||
@@ -266,7 +271,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onEditHandler(record, e.metaKey || e.ctrlKey);
|
||||
onEditHandler(record, { event: e });
|
||||
};
|
||||
|
||||
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
|
||||
@@ -331,7 +336,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
/>,
|
||||
<ColumnButton
|
||||
key="2"
|
||||
onClick={(): void => onEditHandler(record, false)}
|
||||
onClick={(e: React.MouseEvent): void =>
|
||||
onEditHandler(record, { event: e })
|
||||
}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
@@ -339,7 +346,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
</ColumnButton>,
|
||||
<ColumnButton
|
||||
key="3"
|
||||
onClick={(): void => onEditHandler(record, true)}
|
||||
onClick={(): void => onEditHandler(record, { newTab: true })}
|
||||
type="link"
|
||||
loading={editLoader}
|
||||
>
|
||||
|
||||
@@ -416,11 +416,7 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(), '_blank');
|
||||
} else {
|
||||
safeNavigate(getLink());
|
||||
}
|
||||
safeNavigate(getLink(), { event });
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: dashboard.id,
|
||||
dashboardName: dashboard.name,
|
||||
|
||||
@@ -31,6 +31,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -235,7 +236,7 @@ function Application(): JSX.Element {
|
||||
timestamp: number,
|
||||
apmToTraceQuery: Query,
|
||||
isViewLogsClicked?: boolean,
|
||||
): (() => void) => (): void => {
|
||||
): ((e: React.MouseEvent) => void) => (e: React.MouseEvent): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - stepInterval);
|
||||
|
||||
@@ -259,7 +260,11 @@ function Application(): JSX.Element {
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.push(newPath);
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(newPath);
|
||||
} else {
|
||||
history.push(newPath);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[stepInterval],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||
import React, { Dispatch, SetStateAction, useMemo, useRef } from 'react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -42,7 +42,10 @@ interface OnViewTracePopupClickProps {
|
||||
apmToTraceQuery: Query;
|
||||
isViewLogsClicked?: boolean;
|
||||
stepInterval?: number;
|
||||
safeNavigate: (url: string) => void;
|
||||
safeNavigate: (
|
||||
url: string,
|
||||
options?: { event?: React.MouseEvent | MouseEvent },
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface OnViewAPIMonitoringPopupClickProps {
|
||||
@@ -51,8 +54,10 @@ interface OnViewAPIMonitoringPopupClickProps {
|
||||
stepInterval?: number;
|
||||
domainName: string;
|
||||
isError: boolean;
|
||||
|
||||
safeNavigate: (url: string) => void;
|
||||
safeNavigate: (
|
||||
url: string,
|
||||
options?: { event?: React.MouseEvent | MouseEvent },
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function generateExplorerPath(
|
||||
@@ -93,8 +98,8 @@ export function onViewTracePopupClick({
|
||||
isViewLogsClicked,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewTracePopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
}: OnViewTracePopupClickProps): (e?: React.MouseEvent) => void {
|
||||
return (e?: React.MouseEvent): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
|
||||
|
||||
@@ -118,7 +123,7 @@ export function onViewTracePopupClick({
|
||||
queryString,
|
||||
);
|
||||
|
||||
safeNavigate(newPath);
|
||||
safeNavigate(newPath, { event: e });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,8 +154,8 @@ export function onViewAPIMonitoringPopupClick({
|
||||
isError,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
}: OnViewAPIMonitoringPopupClickProps): (e?: React.MouseEvent) => void {
|
||||
return (e?: React.MouseEvent): void => {
|
||||
const endTime = timestamp + (stepInterval || 60);
|
||||
const startTime = timestamp - (stepInterval || 60);
|
||||
const filters = {
|
||||
@@ -190,7 +195,7 @@ export function onViewAPIMonitoringPopupClick({
|
||||
filters,
|
||||
);
|
||||
|
||||
safeNavigate(newPath);
|
||||
safeNavigate(newPath, { event: e });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Bell, Grid } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { DashboardsAndAlertsPopoverProps } from './types';
|
||||
|
||||
@@ -25,9 +26,10 @@ function DashboardsAndAlertsPopover({
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={alert.alert_id}
|
||||
onClick={(): void => {
|
||||
onClick={(e): void => {
|
||||
params.set(QueryParams.ruleId, alert.alert_id);
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
const path = `${ROUTES.ALERT_OVERVIEW}?${params.toString()}`;
|
||||
navigateToPage(path, history.push, e);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
>
|
||||
@@ -55,11 +57,12 @@ function DashboardsAndAlertsPopover({
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={dashboard.dashboard_id}
|
||||
onClick={(): void => {
|
||||
onClick={(e): void => {
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: dashboard.dashboard_id,
|
||||
}),
|
||||
{ event: e },
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
|
||||
@@ -118,6 +118,7 @@ describe('DashboardsAndAlertsPopover', () => {
|
||||
// Should navigate to the dashboard
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/dashboard/${mockDashboard1.dashboard_id}`,
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Spin,
|
||||
@@ -107,8 +107,9 @@ function MetricsTable({
|
||||
onChange: onPaginationChange,
|
||||
total: totalCount,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => openMetricDetails(record.key, 'list'),
|
||||
onRow={(record: MetricsListItemRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void =>
|
||||
openMetricDetails(record.key, 'list', event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
@@ -14,6 +14,7 @@ import { AppState } from 'store/reducers';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import InspectModal from '../Inspect';
|
||||
@@ -209,7 +210,15 @@ function Summary(): JSX.Element {
|
||||
const openMetricDetails = (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set(IS_METRIC_DETAILS_OPEN_KEY, 'true');
|
||||
newParams.set(SELECTED_METRIC_NAME_KEY, metricName);
|
||||
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
setSelectedMetricName(metricName);
|
||||
setIsMetricDetailsOpen(true);
|
||||
setSearchParams({
|
||||
|
||||
@@ -194,7 +194,11 @@ describe('MetricsTable', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Metric 1'));
|
||||
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
|
||||
expect(mockOpenMetricDetails).toHaveBeenCalledWith(
|
||||
'metric1',
|
||||
'list',
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls setOrderBy when column header is clicked', () => {
|
||||
|
||||
@@ -14,7 +14,11 @@ export interface MetricsTableProps {
|
||||
onPaginationChange: (page: number, pageSize: number) => void;
|
||||
setOrderBy: (orderBy: OrderByPayload) => void;
|
||||
totalCount: number;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
openMetricDetails: (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
) => void;
|
||||
queryFilters: TagFilter;
|
||||
}
|
||||
|
||||
@@ -28,7 +32,11 @@ export interface MetricsTreemapProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
viewType: TreemapViewType;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
openMetricDetails: (
|
||||
metricName: string,
|
||||
view: 'list' | 'treemap',
|
||||
event?: React.MouseEvent,
|
||||
) => void;
|
||||
setHeatmapView: (value: TreemapViewType) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Badge, Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { Undo } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { buttonText, RIBBON_STYLES } from './config';
|
||||
|
||||
@@ -21,23 +22,30 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback((): void => {
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
history.push(ROUTES.OLD_LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
history.push(ROUTES.TRACE);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
const onClickHandler = useCallback(
|
||||
(e?: React.MouseEvent): void => {
|
||||
let targetPath: string;
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.OLD_LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
targetPath = ROUTES.TRACES_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
|
||||
targetPath = ROUTES.TRACE;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
navigateToPage(targetPath, history.push, e);
|
||||
},
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<Button
|
||||
icon={<Undo size={16} />}
|
||||
onClick={onClickHandler}
|
||||
onClick={(e): void => onClickHandler(e)}
|
||||
data-testid="newExplorerCTA"
|
||||
type="text"
|
||||
className="periscope-btn link"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
@@ -17,6 +17,7 @@ import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingIn
|
||||
import history from 'lib/history';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import ModuleStepsContainer from './common/ModuleStepsContainer/ModuleStepsContainer';
|
||||
import { stepsMap } from './constants/stepsConfig';
|
||||
@@ -252,9 +253,13 @@ export default function Onboarding(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = (): void => {
|
||||
const handleNext = (e?: React.MouseEvent): void => {
|
||||
if (activeStep <= 3) {
|
||||
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
|
||||
navigateToPage(
|
||||
moduleRouteMap[selectedModule.id as ModulesMap],
|
||||
history.push,
|
||||
e,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -317,9 +322,9 @@ export default function Onboarding(): JSX.Element {
|
||||
{activeStep === 1 && (
|
||||
<div className="onboarding-page">
|
||||
<div
|
||||
onClick={(): void => {
|
||||
onClick={(e): void => {
|
||||
logEvent('Onboarding V2: Skip Button Clicked', {});
|
||||
history.push(ROUTES.APPLICATION);
|
||||
navigateToPage(ROUTES.APPLICATION, history.push, e);
|
||||
}}
|
||||
className="skip-to-console"
|
||||
>
|
||||
@@ -355,7 +360,11 @@ export default function Onboarding(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
<div className="continue-to-next-step">
|
||||
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ArrowRightOutlined />}
|
||||
onClick={(e): void => handleNext(e)}
|
||||
>
|
||||
{t('get_started')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -386,17 +395,16 @@ export default function Onboarding(): JSX.Element {
|
||||
{activeStep > 1 && (
|
||||
<div className="stepsContainer">
|
||||
<ModuleStepsContainer
|
||||
onReselectModule={(): void => {
|
||||
onReselectModule={(e?: React.MouseEvent): void => {
|
||||
setCurrent(current - 1);
|
||||
setActiveStep(activeStep - 1);
|
||||
setSelectedModule(useCases.APM);
|
||||
resetProgress();
|
||||
|
||||
if (isOnboardingV3Enabled) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
} else {
|
||||
history.push(ROUTES.GET_STARTED);
|
||||
}
|
||||
const path = isOnboardingV3Enabled
|
||||
? ROUTES.GET_STARTED_WITH_CLOUD
|
||||
: ROUTES.GET_STARTED;
|
||||
navigateToPage(path, history.push, e);
|
||||
}}
|
||||
selectedModule={selectedModule}
|
||||
selectedModuleSteps={selectedModuleSteps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Blocks, Check } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import './DataSource.styles.scss';
|
||||
@@ -141,13 +142,13 @@ export default function DataSource(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const goToIntegrationsPage = (): void => {
|
||||
const goToIntegrationsPage = (e?: React.MouseEvent): void => {
|
||||
logEvent('Onboarding V2: Go to integrations', {
|
||||
module: selectedModule?.id,
|
||||
dataSource: selectedDataSource?.name,
|
||||
framework: selectedFramework,
|
||||
});
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
navigateToPage(ROUTES.INTEGRATIONS, history.push, e);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -249,7 +250,7 @@ export default function DataSource(): JSX.Element {
|
||||
page which allows more sources of sending data
|
||||
</Typography.Text>
|
||||
<Button
|
||||
onClick={goToIntegrationsPage}
|
||||
onClick={(e): void => goToIntegrationsPage(e)}
|
||||
icon={<Blocks size={14} />}
|
||||
className="navigate-integrations-page-btn"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable react/jsx-no-comment-textnodes */
|
||||
/* eslint-disable sonarjs/prefer-single-boolean-return */
|
||||
import { SetStateAction, useState } from 'react';
|
||||
import React, { SetStateAction, useState } from 'react';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ArrowRightOutlined,
|
||||
@@ -19,6 +19,7 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||
import {
|
||||
@@ -142,7 +143,7 @@ export default function ModuleStepsContainer({
|
||||
return true;
|
||||
};
|
||||
|
||||
const redirectToModules = (): void => {
|
||||
const redirectToModules = (event?: React.MouseEvent): void => {
|
||||
logEvent('Onboarding V2 Complete', {
|
||||
module: selectedModule.id,
|
||||
dataSource: selectedDataSource?.id,
|
||||
@@ -152,26 +153,28 @@ export default function ModuleStepsContainer({
|
||||
serviceName,
|
||||
});
|
||||
|
||||
let targetPath: string;
|
||||
if (selectedModule.id === ModulesMap.APM) {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
} else if (selectedModule.id === ModulesMap.LogsManagement) {
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
targetPath = ROUTES.LOGS_EXPLORER;
|
||||
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
} else if (selectedModule.id === ModulesMap.AwsMonitoring) {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
} else {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
}
|
||||
navigateToPage(targetPath, history.push, event);
|
||||
};
|
||||
|
||||
const handleNext = (): void => {
|
||||
const handleNext = (event?: React.MouseEvent): void => {
|
||||
const isValid = isValidForm();
|
||||
|
||||
if (isValid) {
|
||||
if (current === lastStepIndex) {
|
||||
resetProgress();
|
||||
redirectToModules();
|
||||
redirectToModules(event);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -379,8 +382,8 @@ export default function ModuleStepsContainer({
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoClick = (): void => {
|
||||
history.push('/home');
|
||||
const handleLogoClick = (e: React.MouseEvent): void => {
|
||||
navigateToPage('/home', history.push, e);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -400,7 +403,7 @@ export default function ModuleStepsContainer({
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
type="default"
|
||||
icon={<LeftCircleOutlined />}
|
||||
onClick={onReselectModule}
|
||||
onClick={(e): void => onReselectModule(e)}
|
||||
>
|
||||
{selectedModule.title}
|
||||
</Button>
|
||||
@@ -470,7 +473,11 @@ export default function ModuleStepsContainer({
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
|
||||
<Button
|
||||
onClick={(e): void => handleNext(e)}
|
||||
type="primary"
|
||||
icon={<ArrowRightOutlined />}
|
||||
>
|
||||
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
|
||||
</Button>
|
||||
<LaunchChatSupport
|
||||
|
||||
@@ -21,6 +21,7 @@ import history from 'lib/history';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { CheckIcon, Goal, UserPlus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
|
||||
import InviteTeamMembers from '../InviteTeamMembers/InviteTeamMembers';
|
||||
@@ -413,7 +414,10 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
]);
|
||||
}, [org]);
|
||||
|
||||
const handleUpdateCurrentStep = (step: number): void => {
|
||||
const handleUpdateCurrentStep = (
|
||||
step: number,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
setCurrentStep(step);
|
||||
|
||||
if (step === 1) {
|
||||
@@ -443,43 +447,45 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
...setupStepItemsBase.slice(2),
|
||||
]);
|
||||
} else if (step === 3) {
|
||||
let targetPath: string;
|
||||
switch (selectedDataSource?.module) {
|
||||
case 'apm':
|
||||
history.push(ROUTES.APPLICATION);
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
break;
|
||||
case 'logs':
|
||||
history.push(ROUTES.LOGS);
|
||||
targetPath = ROUTES.LOGS;
|
||||
break;
|
||||
case 'metrics':
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
targetPath = ROUTES.METRICS_EXPLORER;
|
||||
break;
|
||||
case 'dashboards':
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
targetPath = ROUTES.ALL_DASHBOARD;
|
||||
break;
|
||||
case 'infra-monitoring-hosts':
|
||||
history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS);
|
||||
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS;
|
||||
break;
|
||||
case 'infra-monitoring-k8s':
|
||||
history.push(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES);
|
||||
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES;
|
||||
break;
|
||||
case 'messaging-queues-kafka':
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
targetPath = ROUTES.MESSAGING_QUEUES_KAFKA;
|
||||
break;
|
||||
case 'messaging-queues-celery':
|
||||
history.push(ROUTES.MESSAGING_QUEUES_CELERY_TASK);
|
||||
targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK;
|
||||
break;
|
||||
case 'integrations':
|
||||
history.push(ROUTES.INTEGRATIONS);
|
||||
targetPath = ROUTES.INTEGRATIONS;
|
||||
break;
|
||||
case 'home':
|
||||
history.push(ROUTES.HOME);
|
||||
targetPath = ROUTES.HOME;
|
||||
break;
|
||||
case 'api-monitoring':
|
||||
history.push(ROUTES.API_MONITORING);
|
||||
targetPath = ROUTES.API_MONITORING;
|
||||
break;
|
||||
default:
|
||||
history.push(ROUTES.APPLICATION);
|
||||
targetPath = ROUTES.APPLICATION;
|
||||
}
|
||||
navigateToPage(targetPath, history.push, event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -628,7 +634,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
<X
|
||||
size={14}
|
||||
className="onboarding-header-container-close-icon"
|
||||
onClick={(): void => {
|
||||
onClick={(e): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
|
||||
{
|
||||
@@ -636,7 +642,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
history.push(ROUTES.HOME);
|
||||
navigateToPage(ROUTES.HOME, history.push, e);
|
||||
}}
|
||||
/>
|
||||
<Typography.Text>Get Started (2/4)</Typography.Text>
|
||||
@@ -963,7 +969,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
type="primary"
|
||||
disabled={!selectedDataSource}
|
||||
shape="round"
|
||||
onClick={(): void => {
|
||||
onClick={(e): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
|
||||
{
|
||||
@@ -977,7 +983,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
selectedEnvironment || selectedFramework || selectedDataSource;
|
||||
|
||||
if (currentEntity?.internalRedirect && currentEntity?.link) {
|
||||
history.push(currentEntity.link);
|
||||
navigateToPage(currentEntity.link, history.push, e);
|
||||
} else {
|
||||
handleUpdateCurrentStep(2);
|
||||
}
|
||||
@@ -1048,7 +1054,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
<Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={(): void => {
|
||||
onClick={(e): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
|
||||
{
|
||||
@@ -1060,7 +1066,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
);
|
||||
|
||||
handleFilterByCategory('All');
|
||||
handleUpdateCurrentStep(3);
|
||||
handleUpdateCurrentStep(3, e);
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
|
||||
@@ -65,6 +65,7 @@ import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
import { routeConfig } from './config';
|
||||
@@ -306,8 +307,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
icon: <Cog size={16} />,
|
||||
};
|
||||
|
||||
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
|
||||
|
||||
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||
|
||||
const [
|
||||
@@ -436,10 +435,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
|
||||
|
||||
const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
const onClickGetStarted = (event: MouseEvent): void => {
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: '/get-started',
|
||||
@@ -450,7 +445,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
? ROUTES.GET_STARTED_WITH_CLOUD
|
||||
: ROUTES.GET_STARTED;
|
||||
|
||||
if (isCtrlMetaKey(event)) {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openInNewTab(onboaringRoute);
|
||||
} else {
|
||||
history.push(onboaringRoute);
|
||||
@@ -465,7 +460,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isCtrlMetaKey(event)) {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
@@ -664,7 +659,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
|
||||
if (item.key === ROUTES.SETTINGS) {
|
||||
if (isCtrlMetaKey(event)) {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openInNewTab(settingsRoute);
|
||||
} else {
|
||||
history.push(settingsRoute);
|
||||
@@ -842,6 +837,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleHelpSupportMenuItemClick = (info: SidebarItem): void => {
|
||||
const item = helpSupportDropdownMenuItems.find(
|
||||
(item) => !('type' in item) && item.key === info.key,
|
||||
@@ -851,6 +847,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
window.open(item.url, '_blank');
|
||||
}
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
if (item && !('type' in item)) {
|
||||
logEvent('Help Popover: Item clicked', {
|
||||
menuRoute: item.key,
|
||||
@@ -859,10 +857,18 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
switch (item.key) {
|
||||
case ROUTES.SHORTCUTS:
|
||||
history.push(ROUTES.SHORTCUTS);
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.SHORTCUTS);
|
||||
} else {
|
||||
history.push(ROUTES.SHORTCUTS);
|
||||
}
|
||||
break;
|
||||
case 'invite-collaborators':
|
||||
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
} else {
|
||||
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
|
||||
}
|
||||
break;
|
||||
case 'chat-support':
|
||||
if (window.pylon) {
|
||||
@@ -881,6 +887,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleSettingsMenuItemClick = (info: SidebarItem): void => {
|
||||
const item = userSettingsDropdownMenuItems.find(
|
||||
(item) => item?.key === info.key,
|
||||
@@ -898,15 +905,30 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
menuRoute: item?.key,
|
||||
menuLabel,
|
||||
});
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
switch (info.key) {
|
||||
case 'account':
|
||||
history.push(ROUTES.MY_SETTINGS);
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.MY_SETTINGS);
|
||||
} else {
|
||||
history.push(ROUTES.MY_SETTINGS);
|
||||
}
|
||||
break;
|
||||
case 'workspace':
|
||||
history.push(ROUTES.SETTINGS);
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.SETTINGS);
|
||||
} else {
|
||||
history.push(ROUTES.SETTINGS);
|
||||
}
|
||||
break;
|
||||
case 'license':
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(ROUTES.LIST_LICENSES);
|
||||
} else {
|
||||
history.push(ROUTES.LIST_LICENSES);
|
||||
}
|
||||
break;
|
||||
case 'logout':
|
||||
Logout();
|
||||
|
||||
106
frontend/src/hooks/__tests__/useSafeNavigate.test.ts
Normal file
106
frontend/src/hooks/__tests__/useSafeNavigate.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Tests for useSafeNavigate's mock contract.
|
||||
*
|
||||
* The real useSafeNavigate hook is globally replaced by a mock via
|
||||
* jest.config.ts moduleNameMapper, so we cannot test the real
|
||||
* implementation here. Instead we verify:
|
||||
*
|
||||
* 1. The mock accepts the new `event` and `newTab` options without
|
||||
* type errors — ensuring component tests that pass these options
|
||||
* won't break.
|
||||
* 2. The shouldOpenNewTab decision logic (extracted inline below)
|
||||
* matches the real hook's behaviour.
|
||||
*/
|
||||
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isModifierKeyPressed } from 'utils/navigation';
|
||||
|
||||
/**
|
||||
* Mirrors the shouldOpenNewTab logic from the real useSafeNavigate hook.
|
||||
* Kept in sync manually — any drift will be caught by integration tests.
|
||||
*/
|
||||
interface NavigateOptions {
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
const shouldOpenNewTab = (options?: NavigateOptions): boolean =>
|
||||
Boolean(
|
||||
options?.newTab || (options?.event && isModifierKeyPressed(options.event)),
|
||||
);
|
||||
|
||||
describe('useSafeNavigate mock contract', () => {
|
||||
it('mock returns a safeNavigate function', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
expect(typeof safeNavigate).toBe('function');
|
||||
});
|
||||
|
||||
it('safeNavigate accepts string path with event option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
|
||||
expect(() => {
|
||||
safeNavigate('/alerts', { event });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith('/alerts', { event });
|
||||
});
|
||||
|
||||
it('safeNavigate accepts string path with newTab option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
expect(() => {
|
||||
safeNavigate('/dashboard', { newTab: true });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith('/dashboard', { newTab: true });
|
||||
});
|
||||
|
||||
it('safeNavigate accepts SafeNavigateParams with event option', () => {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
|
||||
expect(() => {
|
||||
safeNavigate({ pathname: '/settings', search: '?tab=general' }, { event });
|
||||
}).not.toThrow();
|
||||
|
||||
expect(safeNavigate).toHaveBeenCalledWith(
|
||||
{ pathname: '/settings', search: '?tab=general' },
|
||||
{ event },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldOpenNewTab decision logic', () => {
|
||||
it('returns true when newTab is true', () => {
|
||||
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when event has metaKey pressed', () => {
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when event has ctrlKey pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when event has no modifier keys', () => {
|
||||
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
|
||||
expect(shouldOpenNewTab({ event })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when no options provided', () => {
|
||||
expect(shouldOpenNewTab()).toBe(false);
|
||||
expect(shouldOpenNewTab(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when options provided without event or newTab', () => {
|
||||
expect(shouldOpenNewTab({})).toBe(false);
|
||||
});
|
||||
|
||||
it('newTab takes precedence even without event', () => {
|
||||
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { isModifierKeyPressed } from 'utils/navigation';
|
||||
|
||||
interface NavigateOptions {
|
||||
replace?: boolean;
|
||||
state?: any;
|
||||
newTab?: boolean;
|
||||
event?: MouseEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
interface SafeNavigateParams {
|
||||
@@ -105,6 +107,7 @@ export const useSafeNavigate = (
|
||||
const location = useLocation();
|
||||
|
||||
const safeNavigate = useCallback(
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
|
||||
const currentUrl = new URL(
|
||||
`${location.pathname}${location.search}`,
|
||||
@@ -122,8 +125,10 @@ export const useSafeNavigate = (
|
||||
);
|
||||
}
|
||||
|
||||
// If newTab is true, open in new tab and return early
|
||||
if (options?.newTab) {
|
||||
const shouldOpenNewTab =
|
||||
options?.newTab || (options?.event && isModifierKeyPressed(options.event));
|
||||
|
||||
if (shouldOpenNewTab) {
|
||||
const targetPath =
|
||||
typeof to === 'string'
|
||||
? to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Divider } from 'antd';
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import AlertNotFound from './AlertNotFound';
|
||||
@@ -58,11 +59,11 @@ function BreadCrumbItem({
|
||||
if (isLast) {
|
||||
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
|
||||
}
|
||||
const handleNavigate = (): void => {
|
||||
const handleNavigate = (e: React.MouseEvent): void => {
|
||||
if (!route) {
|
||||
return;
|
||||
}
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
navigateToPage(ROUTES.LIST_ALL_ALERT, history.push, e);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Button, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -15,8 +16,8 @@ function AlertNotFound({ isTestAlert }: AlertNotFoundProps): JSX.Element {
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const checkAllRulesHandler = (): void => {
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT);
|
||||
const checkAllRulesHandler = (e?: React.MouseEvent): void => {
|
||||
safeNavigate(ROUTES.LIST_ALL_ALERT, { event: e });
|
||||
};
|
||||
|
||||
const contactSupportHandler = (): void => {
|
||||
|
||||
@@ -69,7 +69,10 @@ describe('AlertNotFound', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AlertNotFound isTestAlert={false} />);
|
||||
await user.click(screen.getByText('Check all rules'));
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.LIST_ALL_ALERT);
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
expect.objectContaining({ event: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should navigate to the correct support page for cloud users when button is clicked', async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ function AlertTypeSelectionPage(): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const handleSelectType = useCallback(
|
||||
(type: AlertTypes): void => {
|
||||
(type: AlertTypes, event?: React.MouseEvent): void => {
|
||||
// For anamoly based alert, we need to set the ruleType to anomaly_rule
|
||||
// and alertType to metrics_based_alert
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
@@ -41,7 +41,7 @@ function AlertTypeSelectionPage(): JSX.Element {
|
||||
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
|
||||
}
|
||||
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);
|
||||
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { event });
|
||||
},
|
||||
[queryParams, safeNavigate],
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Flex, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { routePermission } from 'utils/permission';
|
||||
|
||||
import './Integrations.styles.scss';
|
||||
|
||||
function Header(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { user } = useAppContext();
|
||||
|
||||
const isGetStartedWithCloudAllowed = routePermission.GET_STARTED_WITH_CLOUD.includes(
|
||||
@@ -30,7 +30,9 @@ function Header(): JSX.Element {
|
||||
<Button
|
||||
className="periscope-btn primary view-data-sources-btn"
|
||||
type="primary"
|
||||
onClick={(): void => history.push(ROUTES.GET_STARTED_WITH_CLOUD)}
|
||||
onClick={(e): void =>
|
||||
navigateToPage(ROUTES.GET_STARTED_WITH_CLOUD, history.push, e)
|
||||
}
|
||||
>
|
||||
<span>View 150+ Data Sources</span>
|
||||
<ArrowRight size={14} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
MessagingQueuesViewType,
|
||||
@@ -64,8 +65,14 @@ function MQDetailPage(): JSX.Element {
|
||||
selectedView !== MessagingQueuesViewType.dropRate.value &&
|
||||
selectedView !== MessagingQueuesViewType.metricPage.value;
|
||||
|
||||
const handleBackClick = (): void => {
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
const handleBackClick = (
|
||||
event?: React.MouseEvent | React.KeyboardEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event as React.MouseEvent)) {
|
||||
openInNewTab(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
} else {
|
||||
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -77,7 +84,7 @@ function MQDetailPage(): JSX.Element {
|
||||
className="message-queue-text"
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleBackClick();
|
||||
handleBackClick(e);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
setConfigDetail,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
import { formatNumericValue } from 'utils/numericUtils';
|
||||
|
||||
import { getTableDataForProducerLatencyOverview } from './MQTableUtils';
|
||||
@@ -80,7 +81,12 @@ export function getColumns(
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
history.push(`/services/${encodeURIComponent(text)}`);
|
||||
const path = `/services/${encodeURIComponent(text)}`;
|
||||
if (isModifierKeyPressed(e)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
@@ -23,26 +24,40 @@ function MessagingQueues(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation('messagingQueuesKafkaOverview');
|
||||
|
||||
const redirectToDetailsPage = (callerView?: string): void => {
|
||||
const redirectToDetailsPage = (
|
||||
callerView?: string,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
logEvent('Messaging Queues: View details clicked', {
|
||||
page: 'Messaging Queues Overview',
|
||||
source: callerView,
|
||||
});
|
||||
|
||||
history.push(
|
||||
`${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
|
||||
);
|
||||
const path = `${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`;
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
};
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const getStartedRedirect = (link: string, sourceCard: string): void => {
|
||||
const getStartedRedirect = (
|
||||
link: string,
|
||||
sourceCard: string,
|
||||
event?: React.MouseEvent,
|
||||
): void => {
|
||||
logEvent('Messaging Queues: Get started clicked', {
|
||||
source: sourceCard,
|
||||
link: isCloudUserVal ? link : KAFKA_SETUP_DOC_LINK,
|
||||
});
|
||||
if (isCloudUserVal) {
|
||||
history.push(link);
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(link);
|
||||
} else {
|
||||
history.push(link);
|
||||
}
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
}
|
||||
@@ -79,10 +94,11 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
onClick={(e): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
|
||||
'Configure Consumer',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -98,10 +114,11 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
onClick={(e): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
|
||||
'Configure Producer',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -117,10 +134,11 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
onClick={(e): void =>
|
||||
getStartedRedirect(
|
||||
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
|
||||
'Monitor kafka',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -143,8 +161,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value, e)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -161,8 +179,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value, e)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -179,8 +197,11 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(
|
||||
MessagingQueuesViewType.partitionLatency.value,
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -197,8 +218,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value, e)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
@@ -215,8 +236,8 @@ function MessagingQueues(): JSX.Element {
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
|
||||
onClick={(e): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value, e)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -184,12 +185,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);
|
||||
@@ -198,7 +193,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('&')}`, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import type { TabsProps } from 'antd';
|
||||
@@ -27,6 +27,7 @@ import { CircleArrowRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import APIError from 'types/api/error';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { navigateToPage } from 'utils/navigation';
|
||||
import { getFormattedDate } from 'utils/timeUtils';
|
||||
|
||||
import CustomerStoryCard from './CustomerStoryCard';
|
||||
@@ -133,10 +134,10 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewBilling = (): void => {
|
||||
const handleViewBilling = (e?: React.MouseEvent): void => {
|
||||
logEvent('Workspace Blocked: User Clicked View Billing', {});
|
||||
|
||||
history.push(ROUTES.BILLING);
|
||||
navigateToPage(ROUTES.BILLING, history.push, e);
|
||||
};
|
||||
|
||||
const renderCustomerStories = (
|
||||
@@ -296,7 +297,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
type="link"
|
||||
size="small"
|
||||
role="button"
|
||||
onClick={handleViewBilling}
|
||||
onClick={(e): void => handleViewBilling(e)}
|
||||
>
|
||||
View Billing
|
||||
</Button>
|
||||
|
||||
123
frontend/src/utils/__tests__/navigation.test.ts
Normal file
123
frontend/src/utils/__tests__/navigation.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
isModifierKeyPressed,
|
||||
navigateToPage,
|
||||
openInNewTab,
|
||||
} from '../navigation';
|
||||
|
||||
describe('navigation utilities', () => {
|
||||
const originalWindowOpen = window.open;
|
||||
|
||||
beforeEach(() => {
|
||||
window.open = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.open = originalWindowOpen;
|
||||
});
|
||||
|
||||
describe('isModifierKeyPressed', () => {
|
||||
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
|
||||
({
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
...overrides,
|
||||
} as MouseEvent);
|
||||
|
||||
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
|
||||
const event = createMouseEvent({ metaKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
|
||||
const event = createMouseEvent({ ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when both metaKey and ctrlKey are pressed', () => {
|
||||
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when neither modifier key is pressed', () => {
|
||||
const event = createMouseEvent();
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when only shiftKey or altKey are pressed', () => {
|
||||
const event = createMouseEvent({
|
||||
shiftKey: true,
|
||||
altKey: true,
|
||||
} as Partial<MouseEvent>);
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openInNewTab', () => {
|
||||
it('calls window.open with the given path and _blank target', () => {
|
||||
openInNewTab('/dashboard');
|
||||
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
|
||||
});
|
||||
|
||||
it('handles full URLs', () => {
|
||||
openInNewTab('https://example.com/page');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://example.com/page',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles paths with query strings', () => {
|
||||
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'/alerts?tab=AlertRules&relativeTime=30m',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToPage', () => {
|
||||
const mockNavigate = jest.fn() as jest.MockedFunction<(path: string) => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockNavigate.mockClear();
|
||||
});
|
||||
|
||||
it('opens new tab when metaKey is pressed', () => {
|
||||
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens new tab when ctrlKey is pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when no modifier key is pressed', () => {
|
||||
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
|
||||
navigateToPage('/services', mockNavigate, event);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/services');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when no event is provided', () => {
|
||||
navigateToPage('/services', mockNavigate);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/services');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls navigate callback when event is undefined', () => {
|
||||
navigateToPage('/dashboard', mockNavigate, undefined);
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
|
||||
expect(window.open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
37
frontend/src/utils/navigation.ts
Normal file
37
frontend/src/utils/navigation.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Returns true if the user is holding Cmd (Mac) or Ctrl (Windows/Linux)
|
||||
* during a click event — the universal "open in new tab" modifier.
|
||||
*/
|
||||
export const isModifierKeyPressed = (
|
||||
event: MouseEvent | React.MouseEvent,
|
||||
): boolean => event.metaKey || event.ctrlKey;
|
||||
|
||||
/**
|
||||
* Opens the given path in a new browser tab.
|
||||
*/
|
||||
export const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to a path, respecting modifier keys. If Cmd/Ctrl is held,
|
||||
* the path is opened in a new tab. Otherwise, the provided `navigate`
|
||||
* callback is invoked for SPA navigation.
|
||||
*
|
||||
* @param path - The target URL path
|
||||
* @param navigate - SPA navigation callback (e.g. history.push, safeNavigate)
|
||||
* @param event - Optional mouse event to check for modifier keys
|
||||
*/
|
||||
export const navigateToPage = (
|
||||
path: string,
|
||||
navigate: (path: string) => void,
|
||||
event?: MouseEvent | React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user