mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-14 16:00:27 +01:00
Compare commits
4 Commits
public-dir
...
base-path-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edf9c6cc1f | ||
|
|
db6da1784d | ||
|
|
07f412f748 | ||
|
|
0216fd61b8 |
@@ -211,6 +211,23 @@ module.exports = {
|
||||
message:
|
||||
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.object.name='window'][callee.property.name='open']",
|
||||
message:
|
||||
'Do not call window.open() directly. ' +
|
||||
"Use openInNewTab() from 'utils/navigation' for internal SigNoz paths. " +
|
||||
"For intentional external URLs, use openExternalLink() from 'utils/navigation'. " +
|
||||
'For unavoidable direct calls, add // eslint-disable-next-line with a reason.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"AssignmentExpression[left.object.name='window'][left.property.name='href']",
|
||||
message:
|
||||
'Do not assign window.location.href for internal navigation. ' +
|
||||
"Use history.push() or history.replace() from 'lib/history'. " +
|
||||
'For external redirects (SSO, logout URLs), add // eslint-disable-next-line with a reason.',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
@@ -263,5 +280,14 @@ module.exports = {
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// navigation.ts and useSafeNavigate.ts are the canonical implementations that call
|
||||
// window.open after computing a base-path-aware href. They are the only places
|
||||
// allowed to call window.open directly.
|
||||
files: ['src/utils/navigation.ts', 'src/hooks/useSafeNavigate.ts'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,3 +6,6 @@ VITE_APPCUES_APP_ID="appcess-app-id"
|
||||
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
|
||||
|
||||
CI="1"
|
||||
|
||||
# Uncomment to test sub-path deployment locally (e.g. app served at /signoz/).
|
||||
# VITE_BASE_PATH="/signoz/"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="<%- BASE_PATH %>" />
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
http-equiv="Cache-Control"
|
||||
@@ -59,7 +60,7 @@
|
||||
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
|
||||
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
|
||||
<link data-react-helmet="true" rel="shortcut icon" href="./favicon.ico" />
|
||||
</head>
|
||||
<body data-theme="default">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
@@ -113,7 +114,7 @@
|
||||
})(document, 'script');
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/css/uPlot.min.css" />
|
||||
<link rel="stylesheet" href="./css/uPlot.min.css" />
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,6 +20,7 @@ const config: Config.InitialOptions = {
|
||||
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
|
||||
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^hooks/useSafeNavigate\\.impl$': '<rootDir>/src/hooks/useSafeNavigate.ts',
|
||||
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||
'^constants/env$': '<rootDir>/__mocks__/env.ts',
|
||||
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { LifeBuoy } from 'lucide-react';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
|
||||
|
||||
@@ -8,7 +9,7 @@ import './AuthHeader.styles.scss';
|
||||
|
||||
function AuthHeader(): JSX.Element {
|
||||
const handleGetHelp = useCallback((): void => {
|
||||
window.open('https://signoz.io/support/', '_blank');
|
||||
openExternalLink('https://signoz.io/support/');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,11 +7,13 @@ import ROUTES from 'constants/routes';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
export interface NavigateToExplorerProps {
|
||||
filters: TagFilterItem[];
|
||||
@@ -133,7 +135,11 @@ export function useNavigateToExplorer(): (
|
||||
QueryParams.compositeQuery
|
||||
}=${JSONCompositeQuery}`;
|
||||
|
||||
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
|
||||
if (sameTab) {
|
||||
history.push(newExplorerPath);
|
||||
} else {
|
||||
openInNewTab(newExplorerPath);
|
||||
}
|
||||
},
|
||||
[
|
||||
prepareQuery,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ChevronsDown, ScrollText } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import ChangelogRenderer from './components/ChangelogRenderer';
|
||||
|
||||
@@ -86,11 +87,7 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
|
||||
}, [checkScroll]);
|
||||
|
||||
const onClickUpdateWorkspace = (): void => {
|
||||
window.open(
|
||||
'https://signoz.io/upgrade-path',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
openExternalLink('https://signoz.io/upgrade-path');
|
||||
};
|
||||
|
||||
const onClickScrollForMore = (): void => {
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
@@ -330,10 +331,7 @@ function HostMetricsDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -352,10 +350,7 @@ function HostMetricsDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import './LearnMore.styles.scss';
|
||||
|
||||
@@ -14,7 +15,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
|
||||
const handleClick = (): void => {
|
||||
onClick?.();
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
openExternalLink(url);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
MessagingQueueHealthCheckService,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import './MessagingQueueHealthCheck.styles.scss';
|
||||
@@ -76,7 +77,7 @@ function ErrorTitleAndKey({
|
||||
if (isCloudUserVal && !!link) {
|
||||
history.push(link);
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
openExternalLink(KAFKA_SETUP_DOC_LINK);
|
||||
}
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import EmptyQuickFilterIcon from 'assets/CustomIcons/EmptyQuickFilterIcon';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
const QUICK_FILTER_DOC_PATHS: Record<string, string> = {
|
||||
severity_text: 'severity-text',
|
||||
@@ -22,9 +23,8 @@ function LogsQuickFilterEmptyState({
|
||||
const handleLearnMoreClick = (): void => {
|
||||
const section = QUICK_FILTER_DOC_PATHS[attributeKey];
|
||||
|
||||
window.open(
|
||||
openExternalLink(
|
||||
`https://signoz.io/docs/logs-management/features/logs-quick-filters#${section}`,
|
||||
'_blank',
|
||||
);
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import { UnfoldVertical } from 'lucide-react';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
|
||||
|
||||
@@ -107,7 +108,7 @@ function DependentServices({
|
||||
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
|
||||
url.search = urlQuery.toString();
|
||||
window.open(url.toString(), '_blank');
|
||||
openInNewTab(`${url.pathname}${url.search}`);
|
||||
},
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import { getOptionList } from './config';
|
||||
import { AlertTypeCard, SelectTypeContainer } from './styles';
|
||||
@@ -55,7 +56,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
page: 'New alert data source selection page',
|
||||
});
|
||||
|
||||
window.open(url, '_blank');
|
||||
openExternalLink(url);
|
||||
}
|
||||
const renderOptions = useMemo(
|
||||
() => (
|
||||
|
||||
@@ -14,6 +14,7 @@ import { IUser } from 'providers/App/types';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { ROUTING_POLICIES_ROUTE } from './constants';
|
||||
import { RoutingPolicyBannerProps } from './types';
|
||||
@@ -387,7 +388,7 @@ export function NotificationChannelsNotFoundContent({
|
||||
style={{ padding: '0 4px' }}
|
||||
type="link"
|
||||
onClick={(): void => {
|
||||
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||
openInNewTab(ROUTES.CHANNELS_NEW);
|
||||
}}
|
||||
>
|
||||
here.
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useGetHosts, usePutHost } from 'api/generated/services/zeus';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import CustomDomainEditModal from './CustomDomainEditModal';
|
||||
|
||||
@@ -48,7 +49,7 @@ function DomainUpdateToast({
|
||||
className="custom-domain-toast-visit-btn"
|
||||
suffixIcon={<ExternalLink size={12} />}
|
||||
onClick={(): void => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
openExternalLink(url);
|
||||
}}
|
||||
>
|
||||
Visit new URL
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
|
||||
import APIError from 'types/api/error';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import './PublicDashboard.styles.scss';
|
||||
|
||||
@@ -294,7 +295,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
icon={<ExternalLink size={12} />}
|
||||
onClick={(): void => {
|
||||
if (publicDashboardURL) {
|
||||
window.open(publicDashboardURL, '_blank');
|
||||
openInNewTab(publicDashboardURL);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import ChannelSelect from './ChannelSelect';
|
||||
@@ -87,7 +88,7 @@ function BasicInfo({
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
ruleId: isNewRule ? 0 : alertDef?.id,
|
||||
});
|
||||
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||
openInNewTab(ROUTES.CHANNELS_NEW);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const hasLoggedEvent = useRef(false);
|
||||
|
||||
@@ -46,6 +46,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import BasicInfo from './BasicInfo';
|
||||
import ChartPreview from './ChartPreview';
|
||||
@@ -771,7 +772,7 @@ function FormAlertRules({
|
||||
queryType: currentQuery.queryType,
|
||||
link: url,
|
||||
});
|
||||
window.open(url, '_blank');
|
||||
openExternalLink(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import beaconUrl from '@/assets/Icons/beacon.svg';
|
||||
import circusTentUrl from '@/assets/Icons/circus-tent.svg';
|
||||
@@ -103,11 +104,7 @@ export default function AlertRules({
|
||||
source: 'Alert Rules',
|
||||
});
|
||||
|
||||
window.open(
|
||||
'https://signoz.io/docs/alerts/',
|
||||
'_blank',
|
||||
'noreferrer noopener',
|
||||
);
|
||||
openExternalLink('https://signoz.io/docs/alerts/');
|
||||
}}
|
||||
>
|
||||
Learn more <ArrowUpRight size={12} />
|
||||
|
||||
@@ -10,6 +10,7 @@ import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openExternalLink, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import circusTentUrl from '@/assets/Icons/circus-tent.svg';
|
||||
import dialsUrl from '@/assets/Icons/dials.svg';
|
||||
@@ -87,10 +88,7 @@ export default function Dashboards({
|
||||
logEvent('Homepage: Learn more clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
window.open(
|
||||
'https://signoz.io/docs/userguide/manage-dashboards/',
|
||||
'_blank',
|
||||
);
|
||||
openExternalLink('https://signoz.io/docs/userguide/manage-dashboards/');
|
||||
}}
|
||||
>
|
||||
Learn more <ArrowUpRight size={12} />
|
||||
@@ -114,7 +112,7 @@ export default function Dashboards({
|
||||
dashboardName: dashboard.data.title,
|
||||
});
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(), '_blank');
|
||||
openInNewTab(getLink());
|
||||
} else {
|
||||
safeNavigate(getLink());
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Link2 } from 'lucide-react';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import containerPlusUrl from '@/assets/Icons/container-plus.svg';
|
||||
import helloWaveUrl from '@/assets/Icons/hello-wave.svg';
|
||||
@@ -51,7 +52,7 @@ function DataSourceInfo({
|
||||
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
} else {
|
||||
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
|
||||
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import './HomeChecklist.styles.scss';
|
||||
|
||||
@@ -99,11 +100,7 @@ function HomeChecklist({
|
||||
) {
|
||||
history.push(item.toRoute || '');
|
||||
} else {
|
||||
window?.open(
|
||||
item.docsLink || '',
|
||||
'_blank',
|
||||
'noopener noreferrer',
|
||||
);
|
||||
openExternalLink(item.docsLink || '');
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -119,7 +116,7 @@ function HomeChecklist({
|
||||
step: item.id,
|
||||
});
|
||||
|
||||
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
|
||||
openExternalLink(item.docsLink || '');
|
||||
}}
|
||||
>
|
||||
<BookOpenText size={16} />
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useAppContext } from 'providers/App/App';
|
||||
import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import circusTentUrl from '@/assets/Icons/circus-tent.svg';
|
||||
import eightBallUrl from '@/assets/Icons/eight-ball.svg';
|
||||
@@ -196,11 +197,7 @@ export default function SavedViews({
|
||||
entity: selectedEntity,
|
||||
});
|
||||
|
||||
window.open(
|
||||
'https://signoz.io/docs/product-features/saved-view/',
|
||||
'_blank',
|
||||
'noopener noreferrer',
|
||||
);
|
||||
openExternalLink('https://signoz.io/docs/product-features/saved-view/');
|
||||
}}
|
||||
>
|
||||
Learn more <ArrowUpRight size={12} />
|
||||
|
||||
@@ -31,6 +31,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
|
||||
|
||||
@@ -79,11 +80,7 @@ const EmptyState = memo(
|
||||
) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
} else {
|
||||
window?.open(
|
||||
DOCS_LINKS.ADD_DATA_SOURCE,
|
||||
'_blank',
|
||||
'noopener noreferrer',
|
||||
);
|
||||
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -97,10 +94,7 @@ const EmptyState = memo(
|
||||
logEvent('Homepage: Learn more clicked', {
|
||||
source: 'Service Metrics',
|
||||
});
|
||||
window.open(
|
||||
'https://signoz.io/docs/instrumentation/overview/',
|
||||
'_blank',
|
||||
);
|
||||
openExternalLink('https://signoz.io/docs/instrumentation/overview/');
|
||||
}}
|
||||
>
|
||||
Learn more <ArrowUpRight size={12} />
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
|
||||
|
||||
@@ -133,11 +134,7 @@ export default function ServiceTraces({
|
||||
) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
} else {
|
||||
window?.open(
|
||||
DOCS_LINKS.ADD_DATA_SOURCE,
|
||||
'_blank',
|
||||
'noopener noreferrer',
|
||||
);
|
||||
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -151,10 +148,7 @@ export default function ServiceTraces({
|
||||
logEvent('Homepage: Learn more clicked', {
|
||||
source: 'Service Traces',
|
||||
});
|
||||
window.open(
|
||||
'https://signoz.io/docs/instrumentation/overview/',
|
||||
'_blank',
|
||||
);
|
||||
openExternalLink('https://signoz.io/docs/instrumentation/overview/');
|
||||
}}
|
||||
>
|
||||
Learn more <ArrowUpRight size={12} />
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
@@ -414,10 +415,7 @@ function ClusterDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -436,10 +434,7 @@ function ClusterDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
@@ -429,10 +430,7 @@ function DaemonSetDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -451,10 +449,7 @@ function DaemonSetDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
@@ -433,10 +434,7 @@ function DeploymentDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -455,10 +453,7 @@ function DeploymentDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import JobEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
@@ -427,10 +428,7 @@ function JobDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -449,10 +447,7 @@ function JobDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
@@ -419,10 +420,7 @@ function NamespaceDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -441,10 +439,7 @@ function NamespaceDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import NodeLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
@@ -416,10 +417,7 @@ function NodeDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -438,10 +436,7 @@ function NodeDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import PodEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
@@ -435,10 +436,7 @@ function PodDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -457,10 +455,7 @@ function PodDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
@@ -431,10 +432,7 @@ function StatefulSetDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
@@ -453,10 +451,7 @@ function StatefulSetDetails({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
interface AlertInfoCardProps {
|
||||
header: string;
|
||||
@@ -19,7 +20,7 @@ function AlertInfoCard({
|
||||
className="alert-info-card"
|
||||
onClick={(): void => {
|
||||
onClick();
|
||||
window.open(link, '_blank');
|
||||
openExternalLink(link);
|
||||
}}
|
||||
>
|
||||
<div className="alert-card-text">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons';
|
||||
import { Flex, Typography } from 'antd';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
interface InfoLinkTextProps {
|
||||
infoText: string;
|
||||
@@ -20,7 +21,7 @@ function InfoLinkText({
|
||||
<Flex
|
||||
onClick={(): void => {
|
||||
onClick();
|
||||
window.open(link, '_blank');
|
||||
openExternalLink(link);
|
||||
}}
|
||||
className="info-link-container"
|
||||
>
|
||||
|
||||
@@ -83,6 +83,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openExternalLink, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
|
||||
@@ -457,7 +458,7 @@ function DashboardsList(): JSX.Element {
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
window.open(getLink(), '_blank');
|
||||
openInNewTab(getLink());
|
||||
}}
|
||||
>
|
||||
Open in New Tab
|
||||
@@ -739,9 +740,8 @@ function DashboardsList(): JSX.Element {
|
||||
className="learn-more"
|
||||
data-testid="learn-more"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
openExternalLink(
|
||||
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LockFilled } from '@ant-design/icons';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { Data } from '../DashboardsList';
|
||||
import { TableLinkText } from './styles';
|
||||
@@ -12,7 +13,7 @@ function Name(name: Data['name'], data: Data): JSX.Element {
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(), '_blank');
|
||||
openInNewTab(getLink());
|
||||
} else {
|
||||
history.push(getLink());
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { useContextLogData } from './useContextLogData';
|
||||
|
||||
@@ -116,7 +117,7 @@ function ContextLogRenderer({
|
||||
);
|
||||
|
||||
const link = `${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`;
|
||||
window.open(link, '_blank', 'noopener,noreferrer');
|
||||
openInNewTab(link);
|
||||
},
|
||||
[query, urlQuery],
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import FieldRenderer from './FieldRenderer';
|
||||
@@ -191,7 +192,7 @@ function TableView({
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
// open the trace in new tab
|
||||
window.open(route, '_blank');
|
||||
openInNewTab(route);
|
||||
} else {
|
||||
history.push(route);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Typography } from 'antd';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
|
||||
@@ -14,7 +15,7 @@ export default function LogsError(): JSX.Element {
|
||||
if (isCloudUserVal) {
|
||||
history.push('/support');
|
||||
} else {
|
||||
window.open('https://signoz.io/slack', '_blank');
|
||||
openExternalLink('https://signoz.io/slack');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { openInNewTab as openInNewTabFn } from 'utils/navigation';
|
||||
|
||||
import { TopOperationList } from './TopOperationsTable';
|
||||
import { NavigateToTraceProps } from './types';
|
||||
@@ -37,7 +38,7 @@ export const navigateToTrace = ({
|
||||
}=${JSONCompositeQuery}`;
|
||||
|
||||
if (openInNewTab) {
|
||||
window.open(newTraceExplorerPath, '_blank');
|
||||
openInNewTabFn(newTraceExplorerPath);
|
||||
} else {
|
||||
safeNavigate(newTraceExplorerPath);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Bell, Grid } from 'lucide-react';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { pluralize } from 'utils/pluralize';
|
||||
|
||||
import { DashboardsAndAlertsPopoverProps } from './types';
|
||||
@@ -67,9 +68,8 @@ function DashboardsAndAlertsPopover({
|
||||
<Typography.Link
|
||||
key={alert.alertId}
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
openInNewTab(
|
||||
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
@@ -90,11 +90,8 @@ function DashboardsAndAlertsPopover({
|
||||
<Typography.Link
|
||||
key={dashboard.dashboardId}
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: dashboard.dashboardId,
|
||||
}),
|
||||
'_blank',
|
||||
openInNewTab(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: dashboard.dashboardId }),
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
|
||||
@@ -6,6 +6,7 @@ import history from 'lib/history';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
|
||||
|
||||
@@ -42,11 +43,11 @@ export default function NoLogs({
|
||||
}
|
||||
history.push(link);
|
||||
} else if (dataSource === 'traces') {
|
||||
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
|
||||
openExternalLink(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE);
|
||||
} else if (dataSource === DataSource.METRICS) {
|
||||
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
|
||||
openExternalLink(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE);
|
||||
} else {
|
||||
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
|
||||
openExternalLink(`${DOCLINKS.USER_GUIDE}${dataSource}/`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -115,6 +115,7 @@ const useBaseAggregateOptions = ({
|
||||
key={id}
|
||||
icon={<LinkOutlined />}
|
||||
onClick={(): void => {
|
||||
// eslint-disable-next-line no-restricted-syntax -- context links can be internal or external URLs provided by users
|
||||
window.open(url, '_blank');
|
||||
onClose?.();
|
||||
}}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ModalTitle } from 'container/PipelinePage/PipelineListsView/styles';
|
||||
import { Check, Loader, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE } from './constants';
|
||||
import {
|
||||
@@ -76,7 +77,7 @@ function RoutingPolicyDetails({
|
||||
style={{ padding: '0 4px' }}
|
||||
type="link"
|
||||
onClick={(): void => {
|
||||
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||
openInNewTab(ROUTES.CHANNELS_NEW);
|
||||
}}
|
||||
>
|
||||
here.
|
||||
|
||||
@@ -64,7 +64,7 @@ import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { openExternalLink, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
|
||||
|
||||
@@ -818,7 +818,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
);
|
||||
|
||||
if (item && !('type' in item) && item.isExternal && item.url) {
|
||||
window.open(item.url, '_blank');
|
||||
openExternalLink(item.url);
|
||||
}
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import noDataUrl from '@/assets/Icons/no-data.svg';
|
||||
@@ -143,7 +144,7 @@ function SpanLogs({
|
||||
|
||||
const url = `${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`;
|
||||
|
||||
window.open(url, '_blank');
|
||||
openInNewTab(url);
|
||||
},
|
||||
[
|
||||
isLogSpanRelated,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { BarChart2, Compass, X } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { RelatedSignalsViews } from '../constants';
|
||||
import SpanLogs from '../SpanLogs/SpanLogs';
|
||||
@@ -157,13 +158,7 @@ function SpanRelatedSignals({
|
||||
searchParams.set(QueryParams.startTime, startTimeMs.toString());
|
||||
searchParams.set(QueryParams.endTime, endTimeMs.toString());
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${
|
||||
ROUTES.LOGS_EXPLORER
|
||||
}?${searchParams.toString()}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${searchParams.toString()}`);
|
||||
}, [selectedSpan.traceId, traceStartTime, traceEndTime]);
|
||||
|
||||
const emptyStateConfig = useMemo(
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
|
||||
} from 'types/actions/trace';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
dayjs.extend(duration);
|
||||
@@ -214,7 +215,7 @@ function TraceTable(): JSX.Element {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(record), '_blank');
|
||||
openInNewTab(getLink(record));
|
||||
} else {
|
||||
history.push(getLink(record));
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import funnelAddUrl from '@/assets/Icons/funnel-add.svg';
|
||||
@@ -547,12 +548,11 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
icon={<ArrowUpRight size={14} />}
|
||||
className="right-info"
|
||||
type="text"
|
||||
onClick={(): WindowProxy | null =>
|
||||
window.open(
|
||||
onClick={(): void => {
|
||||
openExternalLink(
|
||||
'https://signoz.io/docs/userguide/traces/#missing-spans',
|
||||
'_blank',
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import './TracesTableComponent.styles.scss';
|
||||
|
||||
@@ -86,7 +87,7 @@ function TracesTableComponent({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getTraceLink(record), '_blank');
|
||||
openInNewTab(getTraceLink(record));
|
||||
} else {
|
||||
history.push(getTraceLink(record));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ConnectionUrlResponse,
|
||||
GenerateConnectionUrlPayload,
|
||||
} from 'types/api/integrations/aws';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
import { regions } from 'utils/regions';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
@@ -120,7 +121,7 @@ export function useIntegrationModal({
|
||||
logEvent('AWS Integration: Account connection attempt redirected to AWS', {
|
||||
id: data.account_id,
|
||||
});
|
||||
window.open(data.connection_url, '_blank');
|
||||
openExternalLink(data.connection_url);
|
||||
setModalState(ModalStateEnum.WAITING);
|
||||
setAccountId(data.account_id);
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
const queryRangeMutation = useMutation(getSubstituteVars);
|
||||
@@ -92,7 +93,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
|
||||
const url = `${ROUTES.ALERTS_NEW}?${params.toString()}`;
|
||||
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
openInNewTab(url);
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
|
||||
90
frontend/src/hooks/useSafeNavigate.test.ts
Normal file
90
frontend/src/hooks/useSafeNavigate.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom-v5-compat';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
createHref: jest.fn(
|
||||
({
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
}: {
|
||||
pathname: string;
|
||||
search?: string;
|
||||
hash?: string;
|
||||
}) => `/signoz${pathname}${search || ''}${hash || ''}`,
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
import history from 'lib/history';
|
||||
|
||||
import { useSafeNavigate } from './useSafeNavigate';
|
||||
|
||||
function renderSafeNavigate(): ReturnType<typeof useSafeNavigate> {
|
||||
const { result } = renderHook(() => useSafeNavigate(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(MemoryRouter, { initialEntries: ['/home'] }, children),
|
||||
});
|
||||
return result.current;
|
||||
}
|
||||
|
||||
describe('useSafeNavigate — newTab option', () => {
|
||||
let windowOpenSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
windowOpenSpy = jest.spyOn(window, 'open').mockImplementation(jest.fn());
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
windowOpenSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('opens string path in new tab with base path prepended', () => {
|
||||
const { safeNavigate } = renderSafeNavigate();
|
||||
safeNavigate('/traces', { newTab: true });
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith('/signoz/traces', '_blank');
|
||||
expect(history.createHref).toHaveBeenCalledWith({
|
||||
pathname: '/traces',
|
||||
search: '',
|
||||
hash: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves query string when opening in new tab', () => {
|
||||
const { safeNavigate } = renderSafeNavigate();
|
||||
safeNavigate('/traces?service=api', { newTab: true });
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith(
|
||||
'/signoz/traces?service=api',
|
||||
'_blank',
|
||||
);
|
||||
expect(history.createHref).toHaveBeenCalledWith({
|
||||
pathname: '/traces',
|
||||
search: '?service=api',
|
||||
hash: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('opens object-form path in new tab with base path prepended', () => {
|
||||
const { safeNavigate } = renderSafeNavigate();
|
||||
safeNavigate({ pathname: '/logs', search: '?env=prod' }, { newTab: true });
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith('/signoz/logs?env=prod', '_blank');
|
||||
expect(history.createHref).toHaveBeenCalledWith({
|
||||
pathname: '/logs',
|
||||
search: '?env=prod',
|
||||
});
|
||||
});
|
||||
|
||||
it('passes external URL directly to window.open without createHref', () => {
|
||||
const { safeNavigate } = renderSafeNavigate();
|
||||
safeNavigate('https://docs.signoz.io/page', { newTab: true });
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith(
|
||||
'https://docs.signoz.io/page',
|
||||
'_blank',
|
||||
);
|
||||
expect(history.createHref).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
|
||||
import history from 'lib/history';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
|
||||
interface NavigateOptions {
|
||||
@@ -126,11 +127,30 @@ export const useSafeNavigate = (
|
||||
const shouldOpenInNewTab = options?.newTab;
|
||||
|
||||
if (shouldOpenInNewTab) {
|
||||
const targetPath =
|
||||
typeof to === 'string'
|
||||
? to
|
||||
: `${to.pathname || location.pathname}${to.search || ''}`;
|
||||
window.open(targetPath, '_blank');
|
||||
if (!to) {
|
||||
return;
|
||||
}
|
||||
let href: string;
|
||||
if (typeof to === 'string') {
|
||||
// 'to' may include a query string; parse before passing to createHref
|
||||
// so search params are not embedded in pathname.
|
||||
const parsed = new URL(to, window.location.origin);
|
||||
if (parsed.origin !== window.location.origin) {
|
||||
window.open(to, '_blank');
|
||||
return;
|
||||
}
|
||||
href = history.createHref({
|
||||
pathname: parsed.pathname,
|
||||
search: parsed.search,
|
||||
hash: parsed.hash,
|
||||
});
|
||||
} else {
|
||||
href = history.createHref({
|
||||
pathname: to.pathname ?? location.pathname,
|
||||
search: to.search ?? '',
|
||||
});
|
||||
}
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
39
frontend/src/lib/basePath.test.ts
Normal file
39
frontend/src/lib/basePath.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getBasePath, readBasePath } from './basePath';
|
||||
|
||||
describe('readBasePath', () => {
|
||||
afterEach(() => {
|
||||
document.querySelectorAll('base').forEach((el) => el.remove());
|
||||
});
|
||||
|
||||
it('returns "/" when no <base> tag is present', () => {
|
||||
expect(readBasePath()).toBe('/');
|
||||
});
|
||||
|
||||
it('returns the href when <base href="/signoz/"> exists', () => {
|
||||
const base = document.createElement('base');
|
||||
base.setAttribute('href', '/signoz/');
|
||||
document.head.prepend(base);
|
||||
expect(readBasePath()).toBe('/signoz/');
|
||||
});
|
||||
|
||||
it('returns "/" when <base> tag exists but has no href attribute', () => {
|
||||
const base = document.createElement('base');
|
||||
document.head.prepend(base);
|
||||
expect(readBasePath()).toBe('/');
|
||||
});
|
||||
|
||||
it('returns "/" when <base href="/"> exists (root deployment)', () => {
|
||||
const base = document.createElement('base');
|
||||
base.setAttribute('href', '/');
|
||||
document.head.prepend(base);
|
||||
expect(readBasePath()).toBe('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBasePath (module-init snapshot)', () => {
|
||||
it('returns the value captured when the module was first loaded', () => {
|
||||
// In Jest, no <base> tag is present when the module loads, so the
|
||||
// snapshot is '/'. This test documents the singleton contract.
|
||||
expect(getBasePath()).toBe('/');
|
||||
});
|
||||
});
|
||||
21
frontend/src/lib/basePath.ts
Normal file
21
frontend/src/lib/basePath.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Reads the <base href> injected by the backend at serve time (or by
|
||||
* createHtmlPlugin in dev). Returns '/' if no <base> tag is present,
|
||||
* which matches root-path deployments with no backend injection.
|
||||
*
|
||||
* Called once at module init — result is stable for the page lifetime.
|
||||
* Exported for testing; consumers should use getBasePath().
|
||||
*/
|
||||
export function readBasePath(): string {
|
||||
if (typeof document === 'undefined') {
|
||||
return '/';
|
||||
}
|
||||
return document.querySelector('base')?.getAttribute('href') ?? '/';
|
||||
}
|
||||
|
||||
/** @internal Use getBasePath() in application code. */
|
||||
export const basePath: string = readBasePath();
|
||||
|
||||
export function getBasePath(): string {
|
||||
return basePath;
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
export default createBrowserHistory();
|
||||
import { getBasePath } from './basePath';
|
||||
|
||||
// Strip the trailing slash that <base href> includes ('/signoz/' → '/signoz')
|
||||
// because createBrowserHistory expects a basename without trailing slash.
|
||||
const basename = getBasePath().replace(/\/$/, '') || undefined;
|
||||
|
||||
export default createBrowserHistory({ basename });
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { Home, LifeBuoy } from 'lucide-react';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
@@ -11,8 +12,7 @@ import './ErrorBoundaryFallback.styles.scss';
|
||||
|
||||
function ErrorBoundaryFallback(): JSX.Element {
|
||||
const handleReload = (): void => {
|
||||
// Go to home page
|
||||
window.location.href = ROUTES.HOME;
|
||||
history.push(ROUTES.HOME);
|
||||
};
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import history from 'lib/history';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
export const handleContactSupport = (isCloudUser: boolean): void => {
|
||||
if (isCloudUser) {
|
||||
history.push('/support');
|
||||
} else {
|
||||
window.open('https://signoz.io/slack', '_blank');
|
||||
openExternalLink('https://signoz.io/slack');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
convertToMilliseconds,
|
||||
@@ -93,7 +94,7 @@ export function getColumns(
|
||||
key={item}
|
||||
className="traceid-text"
|
||||
onClick={(): void => {
|
||||
window.open(`${ROUTES.TRACE}/${item}`, '_blank');
|
||||
openInNewTab(`${ROUTES.TRACE}/${item}`);
|
||||
logEvent(`MQ Kafka: Drop Rate - traceid navigation`, {
|
||||
item,
|
||||
});
|
||||
@@ -123,7 +124,7 @@ export function getColumns(
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(`/services/${encodeURIComponent(text)}`, '_blank');
|
||||
openInNewTab(`/services/${encodeURIComponent(text)}`);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -10,7 +10,7 @@ import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { openExternalLink, openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
@@ -59,7 +59,7 @@ function MessagingQueues(): JSX.Element {
|
||||
history.push(link);
|
||||
}
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
openExternalLink(KAFKA_SETUP_DOC_LINK);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useAppContext } from 'providers/App/App';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import APIError from 'types/api/error';
|
||||
import { openExternalLink } from 'utils/navigation';
|
||||
|
||||
import './Support.styles.scss';
|
||||
|
||||
@@ -92,7 +93,7 @@ export default function Support(): JSX.Element {
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const handleChannelWithRedirects = (url: string): void => {
|
||||
window.open(url, '_blank');
|
||||
openExternalLink(url);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,80 +1,140 @@
|
||||
import { isModifierKeyPressed } from '../app';
|
||||
import { openInNewTab } from '../navigation';
|
||||
import { openExternalLink, 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 =>
|
||||
// Mock history before importing navigation so history.createHref is controlled.
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
createHref: jest.fn(
|
||||
({
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
button: 0,
|
||||
...overrides,
|
||||
} as MouseEvent);
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
}: {
|
||||
pathname: string;
|
||||
search?: string;
|
||||
hash?: string;
|
||||
}) => `/signoz${pathname}${search || ''}${hash || ''}`,
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
|
||||
const event = createMouseEvent({ metaKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
const mockWindowOpen = jest.fn();
|
||||
Object.defineProperty(window, 'open', {
|
||||
value: mockWindowOpen,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
|
||||
const event = createMouseEvent({ ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
describe('openInNewTab', () => {
|
||||
beforeEach(() => mockWindowOpen.mockClear());
|
||||
|
||||
it('returns true when both metaKey and ctrlKey are pressed', () => {
|
||||
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
// Previously: window.open(path, '_blank') — no base path prepended.
|
||||
// Now: internal paths go through history.createHref so sub-path
|
||||
// deployments (e.g. /signoz/) are handled correctly.
|
||||
|
||||
it('returns false when neither modifier key is pressed', () => {
|
||||
const event = createMouseEvent();
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when only shiftKey or altKey are pressed', () => {
|
||||
const event = createMouseEvent({
|
||||
shiftKey: true,
|
||||
altKey: true,
|
||||
} as Partial<MouseEvent>);
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when middle mouse button is used', () => {
|
||||
const event = createMouseEvent({ button: 1 });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
it('prepends base path to internal path via history.createHref', () => {
|
||||
openInNewTab('/dashboard');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/signoz/dashboard', '_blank');
|
||||
});
|
||||
|
||||
describe('openInNewTab', () => {
|
||||
it('calls window.open with the given path and _blank target', () => {
|
||||
openInNewTab('/dashboard');
|
||||
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
|
||||
});
|
||||
it('preserves query string when prepending base path', () => {
|
||||
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'/signoz/alerts?tab=AlertRules&relativeTime=30m',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles full URLs', () => {
|
||||
openInNewTab('https://example.com/page');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://example.com/page',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
it('preserves hash when prepending base path', () => {
|
||||
openInNewTab('/dashboard/123#panel-5');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'/signoz/dashboard/123#panel-5',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles paths with query strings', () => {
|
||||
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'/alerts?tab=AlertRules&relativeTime=30m',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
// External URLs bypass createHref and are passed through as-is.
|
||||
it('passes http URL directly to window.open without base path', () => {
|
||||
openInNewTab('https://example.com/page');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'https://example.com/page',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('passes protocol-relative URL directly to window.open without base path', () => {
|
||||
openInNewTab('//cdn.example.com/asset.js');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'//cdn.example.com/asset.js',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isModifierKeyPressed', () => {
|
||||
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
|
||||
({
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
button: 0,
|
||||
...overrides,
|
||||
} as MouseEvent);
|
||||
|
||||
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
|
||||
const event = createMouseEvent({ metaKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
|
||||
const event = createMouseEvent({ ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when both metaKey and ctrlKey are pressed', () => {
|
||||
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when neither modifier key is pressed', () => {
|
||||
const event = createMouseEvent();
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when only shiftKey or altKey are pressed', () => {
|
||||
const event = createMouseEvent({
|
||||
shiftKey: true,
|
||||
altKey: true,
|
||||
} as Partial<MouseEvent>);
|
||||
expect(isModifierKeyPressed(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when middle mouse button is used', () => {
|
||||
const event = createMouseEvent({ button: 1 });
|
||||
expect(isModifierKeyPressed(event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openExternalLink', () => {
|
||||
beforeEach(() => mockWindowOpen.mockClear());
|
||||
|
||||
// openExternalLink is new — replaces ad-hoc window.open calls for external
|
||||
// URLs and always adds noopener,noreferrer for security.
|
||||
|
||||
it('opens external URL with noopener,noreferrer', () => {
|
||||
openExternalLink('https://signoz.io/slack');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'https://signoz.io/slack',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens protocol-relative external URL with noopener,noreferrer', () => {
|
||||
openExternalLink('//docs.signoz.io/setup');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'//docs.signoz.io/setup',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
95
frontend/src/utils/navigation.test.ts
Normal file
95
frontend/src/utils/navigation.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
// Mock history before importing navigation, so history.createHref is controlled.
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
createHref: jest.fn(
|
||||
({
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
}: {
|
||||
pathname: string;
|
||||
search?: string;
|
||||
hash?: string;
|
||||
}) => `/signoz${pathname}${search || ''}${hash || ''}`,
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
import { openExternalLink, openInNewTab } from './navigation';
|
||||
|
||||
const mockWindowOpen = jest.fn();
|
||||
Object.defineProperty(window, 'open', {
|
||||
value: mockWindowOpen,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('openInNewTab', () => {
|
||||
beforeEach(() => mockWindowOpen.mockClear());
|
||||
|
||||
it('opens external http URL as-is without prepending base path', () => {
|
||||
openInNewTab('https://signoz.io/docs');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'https://signoz.io/docs',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens external protocol-relative URL as-is', () => {
|
||||
openInNewTab('//cdn.example.com/asset.js');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'//cdn.example.com/asset.js',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('prepends base path to internal path via history.createHref', () => {
|
||||
openInNewTab('/traces');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/signoz/traces', '_blank');
|
||||
});
|
||||
|
||||
it('preserves query string when prepending base path', () => {
|
||||
openInNewTab('/traces?service=frontend&env=prod');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'/signoz/traces?service=frontend&env=prod',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves hash when prepending base path', () => {
|
||||
openInNewTab('/dashboard/123#panel-5');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'/signoz/dashboard/123#panel-5',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles path without leading slash by resolving against origin', () => {
|
||||
openInNewTab('traces');
|
||||
// new URL('traces', window.location.origin) resolves to origin + '/traces'
|
||||
// history.createHref is called with pathname: '/traces'
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/signoz/traces', '_blank');
|
||||
});
|
||||
});
|
||||
|
||||
describe('openExternalLink', () => {
|
||||
beforeEach(() => mockWindowOpen.mockClear());
|
||||
|
||||
it('opens external URL with noopener,noreferrer', () => {
|
||||
openExternalLink('https://signoz.io/slack');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'https://signoz.io/slack',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens protocol-relative external URL', () => {
|
||||
openExternalLink('//docs.signoz.io/setup');
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'//docs.signoz.io/setup',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,37 @@
|
||||
import history from 'lib/history';
|
||||
|
||||
/**
|
||||
* Opens the given path in a new browser tab.
|
||||
* Opens an internal SigNoz path in a new tab.
|
||||
* Automatically prepends the runtime base path via history.createHref so
|
||||
* sub-path deployments (e.g. /signoz/) work correctly.
|
||||
*
|
||||
* For external URLs (http/https), use openExternalLink() instead.
|
||||
*/
|
||||
export const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
if (path.startsWith('http') || path.startsWith('//')) {
|
||||
window.open(path, '_blank');
|
||||
return;
|
||||
}
|
||||
// Parse the path so query params and hash are passed to createHref
|
||||
// separately — passing a full URL string as `pathname` embeds the search
|
||||
// string inside the path segment, which is incorrect.
|
||||
const parsed = new URL(path, window.location.origin);
|
||||
window.open(
|
||||
history.createHref({
|
||||
pathname: parsed.pathname,
|
||||
search: parsed.search,
|
||||
hash: parsed.hash,
|
||||
}),
|
||||
'_blank',
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens an external URL in a new tab with noopener,noreferrer.
|
||||
* Use this for links to external sites (docs, Slack, marketing pages).
|
||||
*
|
||||
* For internal SigNoz routes, use openInNewTab() instead.
|
||||
*/
|
||||
export const openExternalLink = (url: string): void => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ export default defineConfig(
|
||||
data: {
|
||||
PYLON_APP_ID: env.VITE_PYLON_APP_ID || '',
|
||||
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID || '',
|
||||
BASE_PATH: env.VITE_BASE_PATH || '/', // ← REMOVE when BE injection is live
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -81,6 +82,7 @@ export default defineConfig(
|
||||
|
||||
return {
|
||||
plugins,
|
||||
base: './',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
|
||||
Reference in New Issue
Block a user