mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-21 11:20:28 +01:00
Compare commits
27 Commits
feat/mcp-s
...
base-path-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d7d489fb2 | ||
|
|
61ee30b5f0 | ||
|
|
71fd5910a4 | ||
|
|
06e521ad97 | ||
|
|
a9dcad863b | ||
|
|
8bbc5092fa | ||
|
|
1969dc536f | ||
|
|
6e4d6a9511 | ||
|
|
964bd79415 | ||
|
|
a93b26f25e | ||
|
|
ba0a5a1056 | ||
|
|
e9d5012758 | ||
|
|
34a08adde7 | ||
|
|
44cb8f5927 | ||
|
|
50b452080f | ||
|
|
c88c7bac0f | ||
|
|
72db77b068 | ||
|
|
4f84f07494 | ||
|
|
a9e09ee349 | ||
|
|
0a9bb6ba0b | ||
|
|
6664a0fae3 | ||
|
|
040dcb9c9b | ||
|
|
4a39453826 | ||
|
|
ac4db09ec6 | ||
|
|
a691e6a775 | ||
|
|
d270a3807b | ||
|
|
c2b553d26c |
@@ -66,6 +66,8 @@ module.exports = {
|
||||
rules: {
|
||||
// Asset migration — base-path safety
|
||||
'rulesdir/no-unsupported-asset-pattern': 'error',
|
||||
// Base-path safety — window.open and origin-concat patterns
|
||||
'rulesdir/no-raw-absolute-path': 'error',
|
||||
|
||||
// Code quality rules
|
||||
'prefer-const': 'error', // Enforces const for variables never reassigned
|
||||
@@ -213,6 +215,31 @@ module.exports = {
|
||||
message:
|
||||
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[object.name='window'][property.name='localStorage']",
|
||||
message:
|
||||
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"MemberExpression[object.name='window'][property.name='sessionStorage']",
|
||||
message:
|
||||
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
|
||||
},
|
||||
],
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
name: 'localStorage',
|
||||
message:
|
||||
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
|
||||
},
|
||||
{
|
||||
name: 'sessionStorage',
|
||||
message:
|
||||
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
@@ -246,6 +273,11 @@ module.exports = {
|
||||
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
|
||||
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
|
||||
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
|
||||
// Test assertions intentionally reference window.location.origin for expected-value checks
|
||||
'rulesdir/no-raw-absolute-path': 'off',
|
||||
// Tests may access storage directly for setup/assertion/spy purposes
|
||||
'no-restricted-globals': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
153
frontend/eslint-rules/no-raw-absolute-path.js
Normal file
153
frontend/eslint-rules/no-raw-absolute-path.js
Normal file
@@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ESLint rule: no-raw-absolute-path
|
||||
*
|
||||
* Catches patterns that break at runtime when the app is served from a
|
||||
* sub-path (e.g. /signoz/):
|
||||
*
|
||||
* 1. window.open(path, '_blank')
|
||||
* → use openInNewTab(path) which calls withBasePath internally
|
||||
*
|
||||
* 2. window.location.origin + path / `${window.location.origin}${path}`
|
||||
* → use getAbsoluteUrl(path)
|
||||
*
|
||||
* 3. frontendBaseUrl: window.location.origin (bare origin usage)
|
||||
* → use getBaseUrl() to include the base path
|
||||
*
|
||||
* 4. window.location.href = path
|
||||
* → use withBasePath(path) or navigate() for internal navigation
|
||||
*
|
||||
* External URLs (first arg starts with "http") are explicitly allowed.
|
||||
*/
|
||||
|
||||
function isOriginAccess(node) {
|
||||
return (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
node.property.name === 'origin' &&
|
||||
node.object.type === 'MemberExpression' &&
|
||||
!node.object.computed &&
|
||||
node.object.property.name === 'location' &&
|
||||
node.object.object.type === 'Identifier' &&
|
||||
node.object.object.name === 'window'
|
||||
);
|
||||
}
|
||||
|
||||
function isHrefAccess(node) {
|
||||
return (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
node.property.name === 'href' &&
|
||||
node.object.type === 'MemberExpression' &&
|
||||
!node.object.computed &&
|
||||
node.object.property.name === 'location' &&
|
||||
node.object.object.type === 'Identifier' &&
|
||||
node.object.object.name === 'window'
|
||||
);
|
||||
}
|
||||
|
||||
function isExternalUrl(node) {
|
||||
if (node.type === 'Literal' && typeof node.value === 'string') {
|
||||
return node.value.startsWith('http://') || node.value.startsWith('https://');
|
||||
}
|
||||
if (node.type === 'TemplateLiteral' && node.quasis.length > 0) {
|
||||
const raw = node.quasis[0].value.raw;
|
||||
return raw.startsWith('http://') || raw.startsWith('https://');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// window.open(withBasePath(x)) and window.open(getAbsoluteUrl(x)) are already safe.
|
||||
function isSafeHelperCall(node) {
|
||||
return (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
(node.callee.name === 'withBasePath' || node.callee.name === 'getAbsoluteUrl')
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow raw window.open and origin-concatenation patterns that miss the runtime base path',
|
||||
category: 'Base Path Safety',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
windowOpen:
|
||||
'Use openInNewTab(path) instead of window.open(path, "_blank") — openInNewTab prepends the base path automatically.',
|
||||
originConcat:
|
||||
'Use getAbsoluteUrl(path) instead of window.location.origin + path — getAbsoluteUrl prepends the base path automatically.',
|
||||
originDirect:
|
||||
'Use getBaseUrl() instead of window.location.origin — getBaseUrl includes the base path.',
|
||||
hrefAssign:
|
||||
'Use withBasePath(path) or navigate() instead of window.location.href = path — ensures the base path is included.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
// window.open(path, ...) — allow only external first-arg URLs
|
||||
CallExpression(node) {
|
||||
const { callee, arguments: args } = node;
|
||||
if (
|
||||
callee.type !== 'MemberExpression' ||
|
||||
callee.object.type !== 'Identifier' ||
|
||||
callee.object.name !== 'window' ||
|
||||
callee.property.name !== 'open'
|
||||
)
|
||||
return;
|
||||
if (args.length < 1) return;
|
||||
if (isExternalUrl(args[0])) return;
|
||||
if (isSafeHelperCall(args[0])) return;
|
||||
|
||||
context.report({ node, messageId: 'windowOpen' });
|
||||
},
|
||||
|
||||
// window.location.origin + path
|
||||
BinaryExpression(node) {
|
||||
if (node.operator !== '+') return;
|
||||
if (isOriginAccess(node.left) || isOriginAccess(node.right)) {
|
||||
context.report({ node, messageId: 'originConcat' });
|
||||
}
|
||||
},
|
||||
|
||||
// `${window.location.origin}${path}`
|
||||
TemplateLiteral(node) {
|
||||
if (node.expressions.some(isOriginAccess)) {
|
||||
context.report({ node, messageId: 'originConcat' });
|
||||
}
|
||||
},
|
||||
|
||||
// window.location.origin used directly (not in concatenation)
|
||||
// Catches: frontendBaseUrl: window.location.origin
|
||||
MemberExpression(node) {
|
||||
if (!isOriginAccess(node)) return;
|
||||
|
||||
const parent = node.parent;
|
||||
// Skip if parent is BinaryExpression with + (handled by BinaryExpression visitor)
|
||||
if (parent.type === 'BinaryExpression' && parent.operator === '+') return;
|
||||
// Skip if inside TemplateLiteral (handled by TemplateLiteral visitor)
|
||||
if (parent.type === 'TemplateLiteral') return;
|
||||
|
||||
context.report({ node, messageId: 'originDirect' });
|
||||
},
|
||||
|
||||
// window.location.href = path
|
||||
AssignmentExpression(node) {
|
||||
if (node.operator !== '=') return;
|
||||
if (!isHrefAccess(node.left)) return;
|
||||
|
||||
// Allow external URLs
|
||||
if (isExternalUrl(node.right)) return;
|
||||
// Allow safe helper calls
|
||||
if (isSafeHelperCall(node.right)) return;
|
||||
|
||||
context.report({ node, messageId: 'hrefAssign' });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="[[.BaseHref]]" />
|
||||
<meta
|
||||
http-equiv="Cache-Control"
|
||||
content="no-cache, no-store, must-revalidate, max-age: 0"
|
||||
@@ -39,12 +40,12 @@
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
property="og:image"
|
||||
content="/images/signoz-hero-image.webp"
|
||||
content="[[.BaseHref]]images/signoz-hero-image.webp"
|
||||
/>
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
name="twitter:image"
|
||||
content="/images/signoz-hero-image.webp"
|
||||
content="[[.BaseHref]]images/signoz-hero-image.webp"
|
||||
/>
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
@@ -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">
|
||||
<script>
|
||||
@@ -136,7 +137,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>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"page_title": "SigNoz MCP Server",
|
||||
"page_subtitle": "Connect AI assistants like Claude, Cursor, VS Code, and Codex to your SigNoz data via the Model Context Protocol. Authenticate from your MCP client with a service-account API key.",
|
||||
|
||||
"fallback_title": "MCP Server is available on SigNoz Cloud",
|
||||
"fallback_body": "This in-product setup guide is for SigNoz Cloud. Self-hosted users can follow the docs to run the MCP server against their instance.",
|
||||
"fallback_docs_link": "View MCP Server docs",
|
||||
|
||||
"region_warning_prefix": "We couldn't detect your Cloud region from this URL. Find it at ",
|
||||
"region_warning_link": "Settings → Ingestion",
|
||||
"region_warning_suffix": " (the label between your workspace name and signoz.cloud in the ingestion URL), then enter it here.",
|
||||
"region_input_label": "SigNoz Cloud region",
|
||||
"region_input_placeholder": "Your SigNoz Cloud region",
|
||||
|
||||
"step1_title": "Configure your client",
|
||||
"step1_description": "Add SigNoz to your MCP client. Use a one-click install where available, or copy the config for manual setup. On first connect, the client will open a SigNoz authorization page — use the instance URL and API key from step 2.",
|
||||
"step1_manual_fallback": "Or copy the config below for manual setup.",
|
||||
"step1_client_docs_suffix": " setup docs",
|
||||
"step1_add_to_client_prefix": "Add to ",
|
||||
|
||||
"client_cursor_install_label": "Add to Cursor",
|
||||
"client_vscode_install_label": "Add to VS Code",
|
||||
"client_claude_desktop_instructions": "Open Claude Desktop, go to Settings → Connectors → Add custom connector, and paste the endpoint URL above. Claude Desktop does not read remote MCP servers from claude_desktop_config.json — the connector UI is the only supported path.",
|
||||
"client_other_instructions": "Most MCP clients that support remote HTTP servers will accept the endpoint URL above. Add it as a new MCP server in your client and paste your SigNoz API key when the client prompts for authentication. See the docs for client-specific instructions.",
|
||||
|
||||
"step2_title": "Authenticate from your client",
|
||||
"step2_description": "On first connect, your client opens a SigNoz authorization page asking for two values:",
|
||||
"step2_instance_url_label": "SigNoz Instance URL",
|
||||
"step2_api_key_label": "API Key",
|
||||
"step2_admin_cta": "Create service account",
|
||||
"step2_admin_helper": "Create a service account, then add a new key inside it — paste that key into the API Key field.",
|
||||
"step2_viewer_helper": "Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.",
|
||||
|
||||
"use_cases_title": "What you can do with it",
|
||||
"use_cases_item_1": "Ask your AI assistant to investigate a spiking error rate.",
|
||||
"use_cases_item_2": "Debug a slow service by walking through recent traces.",
|
||||
"use_cases_item_3": "Summarize an alert and suggest likely root causes.",
|
||||
"use_cases_item_4": "Generate dashboards or queries from a natural-language description.",
|
||||
"use_cases_docs_link": "See more use cases",
|
||||
|
||||
"copy_tooltip_enabled": "Copy to clipboard",
|
||||
"copy_tooltip_disabled": "Enter your Cloud region first",
|
||||
"copy_aria_endpoint": "Copy MCP endpoint",
|
||||
"copy_aria_instance_url": "Copy SigNoz instance URL",
|
||||
"copy_aria_snippet_prefix": "Copy ",
|
||||
"copy_aria_snippet_suffix": " config",
|
||||
|
||||
"toast_endpoint_copied": "Endpoint copied to clipboard",
|
||||
"toast_snippet_copied": "Snippet copied to clipboard",
|
||||
"toast_instance_url_copied": "Instance URL copied to clipboard",
|
||||
"toast_region_required": "Enter your Cloud region before copying"
|
||||
}
|
||||
@@ -16,6 +16,5 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
"service_accounts": "Service Accounts"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,5 @@
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"page_title": "SigNoz MCP Server",
|
||||
"page_subtitle": "Connect AI assistants like Claude, Cursor, VS Code, and Codex to your SigNoz data via the Model Context Protocol. Authenticate from your MCP client with a service-account API key.",
|
||||
|
||||
"fallback_title": "MCP Server is available on SigNoz Cloud",
|
||||
"fallback_body": "This in-product setup guide is for SigNoz Cloud. Self-hosted users can follow the docs to run the MCP server against their instance.",
|
||||
"fallback_docs_link": "View MCP Server docs",
|
||||
|
||||
"region_warning_prefix": "We couldn't detect your Cloud region from this URL. Find it at ",
|
||||
"region_warning_link": "Settings → Ingestion",
|
||||
"region_warning_suffix": " (the label between your workspace name and signoz.cloud in the ingestion URL), then enter it here.",
|
||||
"region_input_label": "SigNoz Cloud region",
|
||||
"region_input_placeholder": "Your SigNoz Cloud region",
|
||||
|
||||
"step1_title": "Configure your client",
|
||||
"step1_description": "Add SigNoz to your MCP client. Use a one-click install where available, or copy the config for manual setup. On first connect, the client will open a SigNoz authorization page — use the instance URL and API key from step 2.",
|
||||
"step1_manual_fallback": "Or copy the config below for manual setup.",
|
||||
"step1_client_docs_suffix": " setup docs",
|
||||
"step1_add_to_client_prefix": "Add to ",
|
||||
|
||||
"client_cursor_install_label": "Add to Cursor",
|
||||
"client_vscode_install_label": "Add to VS Code",
|
||||
"client_claude_desktop_instructions": "Open Claude Desktop, go to Settings → Connectors → Add custom connector, and paste the endpoint URL above. Claude Desktop does not read remote MCP servers from claude_desktop_config.json — the connector UI is the only supported path.",
|
||||
"client_other_instructions": "Most MCP clients that support remote HTTP servers will accept the endpoint URL above. Add it as a new MCP server in your client and paste your SigNoz API key when the client prompts for authentication. See the docs for client-specific instructions.",
|
||||
|
||||
"step2_title": "Authenticate from your client",
|
||||
"step2_description": "On first connect, your client opens a SigNoz authorization page asking for two values:",
|
||||
"step2_instance_url_label": "SigNoz Instance URL",
|
||||
"step2_api_key_label": "API Key",
|
||||
"step2_admin_cta": "Create service account",
|
||||
"step2_admin_helper": "Create a service account, then add a new key inside it — paste that key into the API Key field.",
|
||||
"step2_viewer_helper": "Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.",
|
||||
|
||||
"use_cases_title": "What you can do with it",
|
||||
"use_cases_item_1": "Ask your AI assistant to investigate a spiking error rate.",
|
||||
"use_cases_item_2": "Debug a slow service by walking through recent traces.",
|
||||
"use_cases_item_3": "Summarize an alert and suggest likely root causes.",
|
||||
"use_cases_item_4": "Generate dashboards or queries from a natural-language description.",
|
||||
"use_cases_docs_link": "See more use cases",
|
||||
|
||||
"copy_tooltip_enabled": "Copy to clipboard",
|
||||
"copy_tooltip_disabled": "Enter your Cloud region first",
|
||||
"copy_aria_endpoint": "Copy MCP endpoint",
|
||||
"copy_aria_instance_url": "Copy SigNoz instance URL",
|
||||
"copy_aria_snippet_prefix": "Copy ",
|
||||
"copy_aria_snippet_suffix": " config",
|
||||
|
||||
"toast_endpoint_copied": "Endpoint copied to clipboard",
|
||||
"toast_snippet_copied": "Snippet copied to clipboard",
|
||||
"toast_instance_url_copied": "Instance URL copied to clipboard",
|
||||
"toast_region_required": "Enter your Cloud region before copying"
|
||||
}
|
||||
@@ -16,6 +16,5 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
"service_accounts": "Service Accounts"
|
||||
}
|
||||
|
||||
@@ -76,6 +76,5 @@
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { initReactI18next } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
|
||||
import cacheBursting from '../../i18n-translations-hash.json';
|
||||
|
||||
@@ -24,7 +25,7 @@ i18n
|
||||
const ns = namespace[0];
|
||||
const pathkey = `/${language}/${ns}`;
|
||||
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
|
||||
return `/locales/${language}/${namespace}.json?h=${hash}`;
|
||||
return `${getBasePath()}locales/${language}/${namespace}.json?h=${hash}`;
|
||||
},
|
||||
},
|
||||
react: {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const get = (key: string): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
return localStorage.getItem(getScopedKey(key));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const remove = (key: string): boolean => {
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
localStorage.removeItem(getScopedKey(key));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const set = (key: string, value: string): boolean => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
localStorage.setItem(getScopedKey(key), value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
||||
12
frontend/src/api/browser/sessionstorage/get.ts
Normal file
12
frontend/src/api/browser/sessionstorage/get.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const get = (key: string): string | null => {
|
||||
try {
|
||||
return sessionStorage.getItem(getScopedKey(key));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
13
frontend/src/api/browser/sessionstorage/remove.ts
Normal file
13
frontend/src/api/browser/sessionstorage/remove.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const remove = (key: string): boolean => {
|
||||
try {
|
||||
sessionStorage.removeItem(getScopedKey(key));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default remove;
|
||||
13
frontend/src/api/browser/sessionstorage/set.ts
Normal file
13
frontend/src/api/browser/sessionstorage/set.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const set = (key: string, value: string): boolean => {
|
||||
try {
|
||||
sessionStorage.setItem(getScopedKey(key), value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default set;
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
interceptorRejected,
|
||||
interceptorsRequestBasePath,
|
||||
interceptorsRequestResponse,
|
||||
interceptorsResponse,
|
||||
} from 'api';
|
||||
@@ -17,6 +18,7 @@ export const GeneratedAPIInstance = <T>(
|
||||
return generatedAPIAxiosInstance({ ...config }).then(({ data }) => data);
|
||||
};
|
||||
|
||||
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||
generatedAPIAxiosInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
|
||||
@@ -11,6 +11,7 @@ import axios, {
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { Events } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
|
||||
@@ -67,6 +68,39 @@ export const interceptorsRequestResponse = (
|
||||
return value;
|
||||
};
|
||||
|
||||
// Strips the leading '/' from path and joins with base — idempotent if already prefixed.
|
||||
// e.g. prependBase('/signoz/', '/api/v1/') → '/signoz/api/v1/'
|
||||
function prependBase(base: string, path: string): string {
|
||||
return path.startsWith(base) ? path : base + path.slice(1);
|
||||
}
|
||||
|
||||
// Prepends the runtime base path to outgoing requests so API calls work under
|
||||
// a URL prefix (e.g. /signoz/api/v1/…). No-op for root deployments and dev
|
||||
// (dev baseURL is a full http:// URL, not an absolute path).
|
||||
export const interceptorsRequestBasePath = (
|
||||
value: InternalAxiosRequestConfig,
|
||||
): InternalAxiosRequestConfig => {
|
||||
const basePath = getBasePath();
|
||||
if (basePath === '/') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.baseURL?.startsWith('/')) {
|
||||
// Production relative baseURL: '/api/v1/' → '/signoz/api/v1/'
|
||||
value.baseURL = prependBase(basePath, value.baseURL);
|
||||
} else if (value.baseURL?.startsWith('http')) {
|
||||
// Dev absolute baseURL (VITE_FRONTEND_API_ENDPOINT): 'https://host/api/v1/' → 'https://host/signoz/api/v1/'
|
||||
const url = new URL(value.baseURL);
|
||||
url.pathname = prependBase(basePath, url.pathname);
|
||||
value.baseURL = url.toString();
|
||||
} else if (!value.baseURL && value.url?.startsWith('/')) {
|
||||
// Orval-generated client (empty baseURL, path in url): '/api/signoz/v1/rules' → '/signoz/api/signoz/v1/rules'
|
||||
value.url = prependBase(basePath, value.url);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const interceptorRejected = async (
|
||||
value: AxiosResponse<any>,
|
||||
): Promise<AxiosResponse<any>> => {
|
||||
@@ -133,6 +167,7 @@ const instance = axios.create({
|
||||
});
|
||||
|
||||
instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
|
||||
|
||||
export const AxiosAlertManagerInstance = axios.create({
|
||||
@@ -147,6 +182,7 @@ ApiV2Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV2Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
|
||||
// axios V3
|
||||
export const ApiV3Instance = axios.create({
|
||||
@@ -158,6 +194,7 @@ ApiV3Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV3Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
// axios V4
|
||||
@@ -170,6 +207,7 @@ ApiV4Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV4Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
// axios V5
|
||||
@@ -182,6 +220,7 @@ ApiV5Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV5Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
// axios Base
|
||||
@@ -194,6 +233,7 @@ LogEventAxiosInstance.interceptors.response.use(
|
||||
interceptorRejectedBase,
|
||||
);
|
||||
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
AxiosAlertManagerInstance.interceptors.response.use(
|
||||
@@ -201,6 +241,7 @@ AxiosAlertManagerInstance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
|
||||
export { apiV1 };
|
||||
export default instance;
|
||||
|
||||
@@ -3,13 +3,16 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
// 10 min in ms
|
||||
const TIMEOUT_IN_MS = 10 * 60 * 1000;
|
||||
|
||||
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
|
||||
new EventSourcePolyfill(
|
||||
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
|
||||
ENVIRONMENT.baseURL
|
||||
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
|
||||
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { withBasePath } from 'utils/basePath';
|
||||
|
||||
export interface NavigateToExplorerProps {
|
||||
filters: TagFilterItem[];
|
||||
@@ -133,7 +134,7 @@ export function useNavigateToExplorer(): (
|
||||
QueryParams.compositeQuery
|
||||
}=${JSONCompositeQuery}`;
|
||||
|
||||
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
|
||||
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[
|
||||
prepareQuery,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CreditCard, MessageSquareText, X } from 'lucide-react';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
export default function ChatSupportGateway(): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
@@ -54,7 +55,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import DeleteMemberDialog from './DeleteMemberDialog';
|
||||
@@ -387,7 +388,7 @@ function EditMemberDrawer({
|
||||
pathParams: { id: member.id },
|
||||
});
|
||||
if (response?.data?.token) {
|
||||
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
|
||||
const link = getAbsoluteUrl(`/password-reset?token=${response.data.token}`);
|
||||
setResetLink(link);
|
||||
setResetLinkExpiresAt(
|
||||
response.data.expiresAt
|
||||
|
||||
@@ -13,6 +13,7 @@ import GetMinMax from 'lib/getMinMax';
|
||||
import { Check, Info, Link2 } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
const routesToBeSharedWithTime = [
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
@@ -80,17 +81,13 @@ function ShareURLModal(): JSX.Element {
|
||||
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
|
||||
currentUrl = `${window.location.origin}${
|
||||
location.pathname
|
||||
}?${urlQuery.toString()}`;
|
||||
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, selectedTime);
|
||||
currentUrl = `${window.location.origin}${
|
||||
location.pathname
|
||||
}?${urlQuery.toString()}`;
|
||||
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { ROLES } from 'types/roles';
|
||||
import { EMAIL_REGEX } from 'utils/app';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -188,7 +189,7 @@ function InviteMembersModal({
|
||||
email: row.email.trim(),
|
||||
name: '',
|
||||
role: row.role as ROLES,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
});
|
||||
} else {
|
||||
await inviteUsers({
|
||||
@@ -196,7 +197,7 @@ function InviteMembersModal({
|
||||
email: row.email.trim(),
|
||||
name: '',
|
||||
role: row.role,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,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 { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
import './LaunchChatSupport.styles.scss';
|
||||
|
||||
@@ -154,7 +155,7 @@ function LaunchChatSupport({
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { openInNewTab } 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');
|
||||
openInNewTab(url);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
MessagingQueueHealthCheckService,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { openInNewTab } 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');
|
||||
openInNewTab(KAFKA_SETUP_DOC_LINK);
|
||||
}
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import getSessionStorageApi from 'api/browser/sessionstorage/get';
|
||||
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
|
||||
import setSessionStorageApi from 'api/browser/sessionstorage/set';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const PREVIOUS_QUERY_KEY = 'previousQuery';
|
||||
|
||||
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
|
||||
const raw = getSessionStorageApi(PREVIOUS_QUERY_KEY);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
@@ -17,7 +20,7 @@ function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
|
||||
|
||||
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
|
||||
try {
|
||||
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
|
||||
setSessionStorageApi(PREVIOUS_QUERY_KEY, JSON.stringify(store));
|
||||
} catch {
|
||||
// ignore quota or serialization errors
|
||||
}
|
||||
@@ -63,7 +66,7 @@ export const removeKeyFromPreviousQuery = (key: string): void => {
|
||||
|
||||
export const clearPreviousQuery = (): void => {
|
||||
try {
|
||||
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
|
||||
removeSessionStorageApi(PREVIOUS_QUERY_KEY);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ function DynamicColumnTable({
|
||||
// Update URL with new page number while preserving other params
|
||||
urlQuery.set('page', page.toString());
|
||||
|
||||
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(newUrl);
|
||||
safeNavigate({ search: `?${urlQuery.toString()}` });
|
||||
|
||||
// Call original pagination handler if provided
|
||||
if (pagination?.onChange && !!pageSize) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
|
||||
import { DynamicColumnsKey } from './contants';
|
||||
import {
|
||||
GetNewColumnDataFunction,
|
||||
@@ -12,7 +15,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
|
||||
}) => {
|
||||
let columnVisibilityData: { [key: string]: boolean };
|
||||
try {
|
||||
const storedData = localStorage.getItem(tablesource);
|
||||
const storedData = getLocalStorageKey(tablesource);
|
||||
if (typeof storedData === 'string' && dynamicColumns) {
|
||||
columnVisibilityData = JSON.parse(storedData);
|
||||
return dynamicColumns.filter((column) => {
|
||||
@@ -28,7 +31,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
|
||||
initialColumnVisibility[key] = false;
|
||||
});
|
||||
|
||||
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
|
||||
setLocalStorageKey(tablesource, JSON.stringify(initialColumnVisibility));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -42,14 +45,14 @@ export const setVisibleColumns = ({
|
||||
dynamicColumns,
|
||||
}: SetVisibleColumnsProps): void => {
|
||||
try {
|
||||
const storedData = localStorage.getItem(tablesource);
|
||||
const storedData = getLocalStorageKey(tablesource);
|
||||
if (typeof storedData === 'string' && dynamicColumns) {
|
||||
const columnVisibilityData = JSON.parse(storedData);
|
||||
const { key } = dynamicColumns[index];
|
||||
if (key) {
|
||||
columnVisibilityData[key] = checked;
|
||||
}
|
||||
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
|
||||
setLocalStorageKey(tablesource, JSON.stringify(columnVisibilityData));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -87,7 +87,6 @@ const ROUTES = {
|
||||
HOME_PAGE: '/',
|
||||
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
|
||||
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
|
||||
MCP_SERVER: '/settings/mcp-server',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -94,20 +95,14 @@ function DependentServices({
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
const url = new URL(
|
||||
`/services/${
|
||||
record.serviceData.serviceName &&
|
||||
record.serviceData.serviceName !== '-'
|
||||
? record.serviceData.serviceName
|
||||
: ''
|
||||
}`,
|
||||
window.location.origin,
|
||||
);
|
||||
const serviceName =
|
||||
record.serviceData.serviceName && record.serviceData.serviceName !== '-'
|
||||
? record.serviceData.serviceName
|
||||
: '';
|
||||
const urlQuery = new URLSearchParams();
|
||||
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
|
||||
url.search = urlQuery.toString();
|
||||
window.open(url.toString(), '_blank');
|
||||
openInNewTab(`/services/${serviceName}?${urlQuery.toString()}`);
|
||||
},
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import {
|
||||
@@ -461,7 +462,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const handleFailedPayment = useCallback((): void => {
|
||||
manageCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
}, [manageCreditCard]);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { isEmpty, pick } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||
@@ -324,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
} else {
|
||||
logEvent('Billing : Manage Billing', {
|
||||
@@ -333,7 +334,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
});
|
||||
|
||||
manageCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -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 { openInNewTab } 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');
|
||||
openInNewTab(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.
|
||||
|
||||
@@ -48,6 +48,7 @@ function DomainUpdateToast({
|
||||
className="custom-domain-toast-visit-btn"
|
||||
suffixIcon={<ExternalLink size={12} />}
|
||||
onClick={(): void => {
|
||||
// eslint-disable-next-line rulesdir/no-raw-absolute-path
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -16,6 +16,8 @@ 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 { getAbsoluteUrl } from 'utils/basePath';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import './PublicDashboard.styles.scss';
|
||||
|
||||
@@ -213,7 +215,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
|
||||
try {
|
||||
setCopyPublicDashboardURL(
|
||||
`${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
|
||||
getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
|
||||
);
|
||||
toast.success('Copied Public Dashboard URL successfully');
|
||||
} catch (error) {
|
||||
@@ -222,7 +224,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
};
|
||||
|
||||
const publicDashboardURL = useMemo(
|
||||
() => `${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
|
||||
() => getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
|
||||
[publicDashboardResponse],
|
||||
);
|
||||
|
||||
@@ -294,7 +296,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
icon={<ExternalLink size={12} />}
|
||||
onClick={(): void => {
|
||||
if (publicDashboardURL) {
|
||||
window.open(publicDashboardURL, '_blank');
|
||||
openInNewTab(publicDashboardURL);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import getSessionStorageApi from 'api/browser/sessionstorage/get';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -26,7 +27,7 @@ function DashboardBreadcrumbs(): JSX.Element {
|
||||
const { title = '', image = Base64Icons[0] } = selectedData || {};
|
||||
|
||||
const goToListPage = useCallback(() => {
|
||||
const dashboardsListQueryParamsString = sessionStorage.getItem(
|
||||
const dashboardsListQueryParamsString = getSessionStorageApi(
|
||||
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import removeLocalStorageKey from 'api/browser/localstorage/remove';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import { GraphVisibilityState, SeriesVisibilityItem } from '../types';
|
||||
@@ -12,7 +15,7 @@ export function getStoredSeriesVisibility(
|
||||
widgetId: string,
|
||||
): SeriesVisibilityItem[] | null {
|
||||
try {
|
||||
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
|
||||
if (!storedData) {
|
||||
return null;
|
||||
@@ -29,7 +32,7 @@ export function getStoredSeriesVisibility(
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
// If the stored data is malformed, remove it
|
||||
localStorage.removeItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
removeLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
}
|
||||
// Silently handle parsing errors - fall back to default visibility
|
||||
return null;
|
||||
@@ -42,7 +45,7 @@ export function updateSeriesVisibilityToLocalStorage(
|
||||
): void {
|
||||
let visibilityStates: GraphVisibilityState[] = [];
|
||||
try {
|
||||
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
visibilityStates = JSON.parse(storedData || '[]');
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
@@ -63,7 +66,7 @@ export function updateSeriesVisibilityToLocalStorage(
|
||||
];
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
JSON.stringify(visibilityStates),
|
||||
);
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import axios from 'axios';
|
||||
@@ -472,7 +474,7 @@ function ExplorerOptions({
|
||||
value: string;
|
||||
}): void => {
|
||||
// Retrieve stored views from local storage
|
||||
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
|
||||
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
|
||||
|
||||
// Initialize or parse the stored views
|
||||
const updatedViews: PreservedViewsInLocalStorage = storedViews
|
||||
@@ -486,7 +488,7 @@ function ExplorerOptions({
|
||||
};
|
||||
|
||||
// Save the updated views back to local storage
|
||||
localStorage.setItem(
|
||||
setLocalStorageKey(
|
||||
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(updatedViews),
|
||||
);
|
||||
@@ -537,7 +539,7 @@ function ExplorerOptions({
|
||||
|
||||
const removeCurrentViewFromLocalStorage = (): void => {
|
||||
// Retrieve stored views from local storage
|
||||
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
|
||||
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
|
||||
|
||||
if (storedViews) {
|
||||
// Parse the stored views
|
||||
@@ -547,7 +549,7 @@ function ExplorerOptions({
|
||||
delete parsedViews[PRESERVED_VIEW_TYPE];
|
||||
|
||||
// Update local storage with the modified views
|
||||
localStorage.setItem(
|
||||
setLocalStorageKey(
|
||||
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(parsedViews),
|
||||
);
|
||||
@@ -672,7 +674,7 @@ function ExplorerOptions({
|
||||
}
|
||||
|
||||
const parsedPreservedView = JSON.parse(
|
||||
localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
|
||||
getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
|
||||
);
|
||||
|
||||
const preservedView = parsedPreservedView[PRESERVED_VIEW_TYPE] || {};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { showErrorNotification } from 'components/ExplorerCard/utils';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -71,7 +73,7 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
|
||||
|
||||
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
|
||||
try {
|
||||
const showExplorerToolbar = localStorage.getItem(
|
||||
const showExplorerToolbar = getLocalStorageKey(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
);
|
||||
if (showExplorerToolbar === null) {
|
||||
@@ -84,7 +86,7 @@ export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
|
||||
[DataSource.TRACES]: true,
|
||||
[DataSource.LOGS]: true,
|
||||
};
|
||||
localStorage.setItem(
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
JSON.stringify(parsedShowExplorerToolbar),
|
||||
);
|
||||
@@ -103,13 +105,13 @@ export const setExplorerToolBarVisibility = (
|
||||
dataSource: string,
|
||||
): void => {
|
||||
try {
|
||||
const showExplorerToolbar = localStorage.getItem(
|
||||
const showExplorerToolbar = getLocalStorageKey(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
);
|
||||
if (showExplorerToolbar) {
|
||||
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
|
||||
parsedShowExplorerToolbar[dataSource] = value;
|
||||
localStorage.setItem(
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
JSON.stringify(parsedShowExplorerToolbar),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import APIError from 'types/api/error';
|
||||
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
import tvUrl from '@/assets/svgs/tv.svg';
|
||||
|
||||
@@ -105,7 +106,7 @@ function ForgotPassword({
|
||||
data: {
|
||||
email: values.email,
|
||||
orgId: currentOrgId,
|
||||
frontendBaseURL: window.location.origin,
|
||||
frontendBaseURL: getBaseUrl(),
|
||||
},
|
||||
});
|
||||
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);
|
||||
|
||||
@@ -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 { openInNewTab } 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');
|
||||
openInNewTab(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
@@ -100,7 +102,7 @@ export const saveLegendEntriesToLocalStorage = ({
|
||||
|
||||
try {
|
||||
existingEntries = JSON.parse(
|
||||
localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
|
||||
getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error parsing LEGEND_GRAPH from local storage', error);
|
||||
@@ -115,7 +117,7 @@ export const saveLegendEntriesToLocalStorage = ({
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
JSON.stringify(existingEntries),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { NavigateToExplorerProps } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -44,8 +45,8 @@ export const getLocalStorageGraphVisibilityState = ({
|
||||
],
|
||||
};
|
||||
|
||||
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||
const legendGraphFromLocalStore = localStorage.getItem(
|
||||
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||
const legendGraphFromLocalStore = getLocalStorageKey(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
);
|
||||
let legendFromLocalStore: {
|
||||
@@ -94,8 +95,8 @@ export const getGraphVisibilityStateOnDataChange = ({
|
||||
graphVisibilityStates: Array(options.series.length).fill(true),
|
||||
legendEntry: showAllDataSet(options),
|
||||
};
|
||||
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||
const legendGraphFromLocalStore = localStorage.getItem(
|
||||
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||
const legendGraphFromLocalStore = getLocalStorageKey(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
);
|
||||
let legendFromLocalStore: {
|
||||
|
||||
@@ -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 { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import dialsUrl from '@/assets/Icons/dials.svg';
|
||||
|
||||
@@ -114,7 +115,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 { openInNewTab } 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');
|
||||
openInNewTab(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 { openInNewTab } 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',
|
||||
);
|
||||
openInNewTab(item.docsLink || '');
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -119,7 +116,7 @@ function HomeChecklist({
|
||||
step: item.id,
|
||||
});
|
||||
|
||||
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
|
||||
openInNewTab(item.docsLink ?? '');
|
||||
}}
|
||||
>
|
||||
<BookOpenText size={16} />
|
||||
|
||||
@@ -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 { openInNewTab } 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',
|
||||
);
|
||||
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 { openInNewTab } 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',
|
||||
);
|
||||
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { filterDuplicateFilters } from '../commonUtils';
|
||||
@@ -569,10 +570,7 @@ function K8sBaseDetails<T>({
|
||||
|
||||
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,
|
||||
@@ -591,10 +589,7 @@ function K8sBaseDetails<T>({
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CloudintegrationtypesServiceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
import './ServiceDashboards.styles.scss';
|
||||
|
||||
@@ -44,7 +45,11 @@ function ServiceDashboards({
|
||||
return;
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
|
||||
window.open(
|
||||
withBasePath(dashboardUrl),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
return;
|
||||
}
|
||||
safeNavigate(dashboardUrl);
|
||||
@@ -54,7 +59,11 @@ function ServiceDashboards({
|
||||
return;
|
||||
}
|
||||
if (event.button === 1) {
|
||||
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
|
||||
window.open(
|
||||
withBasePath(dashboardUrl),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event): void => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
interface AlertInfoCardProps {
|
||||
header: string;
|
||||
@@ -19,7 +20,7 @@ function AlertInfoCard({
|
||||
className="alert-info-card"
|
||||
onClick={(): void => {
|
||||
onClick();
|
||||
window.open(link, '_blank');
|
||||
openInNewTab(link);
|
||||
}}
|
||||
>
|
||||
<div className="alert-card-text">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons';
|
||||
import { Flex, Typography } from 'antd';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
interface InfoLinkTextProps {
|
||||
infoText: string;
|
||||
@@ -20,7 +21,7 @@ function InfoLinkText({
|
||||
<Flex
|
||||
onClick={(): void => {
|
||||
onClick();
|
||||
window.open(link, '_blank');
|
||||
openInNewTab(link);
|
||||
}}
|
||||
className="info-link-container"
|
||||
>
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd/lib';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/v1/dashboards/create';
|
||||
import { AxiosError } from 'axios';
|
||||
@@ -83,6 +85,8 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
|
||||
@@ -145,7 +149,7 @@ function DashboardsList(): JSX.Element {
|
||||
);
|
||||
|
||||
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
|
||||
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
|
||||
const dashboardDynamicColumnsString = getLocalStorageKey('dashboard');
|
||||
let dashboardDynamicColumns: DashboardDynamicColumns = {
|
||||
createdAt: true,
|
||||
createdBy: true,
|
||||
@@ -159,7 +163,7 @@ function DashboardsList(): JSX.Element {
|
||||
);
|
||||
|
||||
if (isEmpty(tempDashboardDynamicColumns)) {
|
||||
localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns));
|
||||
setLocalStorageKey('dashboard', JSON.stringify(dashboardDynamicColumns));
|
||||
} else {
|
||||
dashboardDynamicColumns = { ...tempDashboardDynamicColumns };
|
||||
}
|
||||
@@ -167,7 +171,7 @@ function DashboardsList(): JSX.Element {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns));
|
||||
setLocalStorageKey('dashboard', JSON.stringify(dashboardDynamicColumns));
|
||||
}
|
||||
|
||||
return dashboardDynamicColumns;
|
||||
@@ -181,7 +185,7 @@ function DashboardsList(): JSX.Element {
|
||||
visibleColumns: DashboardDynamicColumns,
|
||||
): void {
|
||||
try {
|
||||
localStorage.setItem('dashboard', JSON.stringify(visibleColumns));
|
||||
setLocalStorageKey('dashboard', JSON.stringify(visibleColumns));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -457,7 +461,7 @@ function DashboardsList(): JSX.Element {
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
window.open(getLink(), '_blank');
|
||||
openInNewTab(getLink());
|
||||
}}
|
||||
>
|
||||
Open in New Tab
|
||||
@@ -469,7 +473,7 @@ function DashboardsList(): JSX.Element {
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(`${window.location.origin}${getLink()}`);
|
||||
setCopy(getAbsoluteUrl(getLink()));
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
|
||||
@@ -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 { withBasePath } from 'utils/basePath';
|
||||
|
||||
import { useContextLogData } from './useContextLogData';
|
||||
|
||||
@@ -116,7 +117,7 @@ function ContextLogRenderer({
|
||||
);
|
||||
|
||||
const link = `${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`;
|
||||
window.open(link, '_blank', 'noopener,noreferrer');
|
||||
window.open(withBasePath(link), '_blank', 'noopener,noreferrer');
|
||||
},
|
||||
[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);
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@ function Login(): JSX.Element {
|
||||
if (isCallbackAuthN) {
|
||||
const url = form.getFieldValue('url');
|
||||
|
||||
// eslint-disable-next-line rulesdir/no-raw-absolute-path
|
||||
window.location.href = url;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import ROUTES from 'constants/routes';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
import { infinityDefaultStyles } from '../InfinityTableView/config';
|
||||
import { TanStackTableStyled } from '../InfinityTableView/styles';
|
||||
@@ -239,7 +240,7 @@ const TanStackTableView = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
|
||||
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
|
||||
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
|
||||
|
||||
setCopy(link);
|
||||
toast.success('Copied to clipboard', { position: 'top-right' });
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
.mcp-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
max-width: 55rem;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--l0-foreground);
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
border: 0.0625rem solid var(--l3-background);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
&__card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--l0-foreground);
|
||||
}
|
||||
|
||||
&__step-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-robin-400);
|
||||
color: var(--l0-background);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__card-description {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
&__endpoint-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__endpoint-field {
|
||||
flex: 1;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
&__endpoint-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 0.0625rem solid var(--l3-background);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--l2-background);
|
||||
font-family: var(--font-family-monospace, monospace);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l0-foreground);
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
cursor: pointer;
|
||||
color: var(--l2-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
&__region-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border: 0.0625rem solid var(--bg-amber-500);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--bg-amber-500-a10, rgba(255, 193, 7, 0.08));
|
||||
font-size: 0.75rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.125rem;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-amber-500);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__region-card,
|
||||
&__cta-card {
|
||||
padding: 1rem 1.25rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&__auth-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
&__auth-field-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--l2-foreground);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
&__info-banner-inline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-left: 0.1875rem solid var(--bg-robin-400);
|
||||
background: var(--l2-background);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-robin-400);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__endpoint-input {
|
||||
margin-top: 0.5rem;
|
||||
font-family: var(--font-family-monospace, monospace);
|
||||
}
|
||||
|
||||
&__cta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__helper-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.125rem;
|
||||
}
|
||||
|
||||
&__tabs-container {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__snippet-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&__install-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&__snippet-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__use-cases {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
&-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
|
||||
li {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l1-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
border: 0.0625rem dashed var(--l3-background);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--l1-background);
|
||||
max-width: 40rem;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--l0-foreground);
|
||||
}
|
||||
|
||||
&-body {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { TFunction, useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button, Input, Tabs, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LearnMore from 'components/LearnMore/LearnMore';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
Info,
|
||||
KeyRound,
|
||||
Sparkles,
|
||||
TriangleAlert,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import {
|
||||
docsUrl,
|
||||
MCP_CLIENTS,
|
||||
MCP_DOCS_URL,
|
||||
MCP_USE_CASES_URL,
|
||||
McpClient,
|
||||
} from './clients';
|
||||
import {
|
||||
buildMcpEndpoint,
|
||||
getCloudRegion,
|
||||
normalizeRegion,
|
||||
parseRegionFromUrl,
|
||||
} from './getCloudRegion';
|
||||
|
||||
import './MCPServerSettings.styles.scss';
|
||||
|
||||
const ANALYTICS = {
|
||||
PAGE_VIEWED: 'MCP Settings: Page viewed',
|
||||
CREATE_SA_CLICKED: 'MCP Settings: Create service account clicked',
|
||||
CLIENT_TAB_SELECTED: 'MCP Settings: Client tab selected',
|
||||
SNIPPET_COPIED: 'MCP Settings: Client snippet copied',
|
||||
ONE_CLICK_INSTALL_CLICKED: 'MCP Settings: One-click install clicked',
|
||||
INSTANCE_URL_COPIED: 'MCP Settings: Instance URL copied',
|
||||
DOCS_LINK_CLICKED: 'MCP Settings: Docs link clicked',
|
||||
} as const;
|
||||
|
||||
const ENDPOINT_PLACEHOLDER = 'https://mcp.<region>.signoz.cloud/mcp';
|
||||
|
||||
function NotCloudFallback(): JSX.Element {
|
||||
const { t } = useTranslation('mcpServer');
|
||||
const onClick = useCallback(() => {
|
||||
logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target: 'fallback' });
|
||||
}, []);
|
||||
return (
|
||||
<div className="mcp-settings">
|
||||
<div className="mcp-settings__fallback">
|
||||
<div className="mcp-settings__fallback-title">
|
||||
<Sparkles size={18} /> {t('fallback_title')}
|
||||
</div>
|
||||
<Typography.Text className="mcp-settings__fallback-body">
|
||||
{t('fallback_body')}
|
||||
</Typography.Text>
|
||||
<LearnMore
|
||||
text={t('fallback_docs_link')}
|
||||
url={MCP_DOCS_URL}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CopyIconButtonProps {
|
||||
ariaLabel: string;
|
||||
onCopy: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function CopyIconButton({
|
||||
ariaLabel,
|
||||
onCopy,
|
||||
disabled,
|
||||
}: CopyIconButtonProps): JSX.Element {
|
||||
const { t } = useTranslation('mcpServer');
|
||||
const tooltipTitle = disabled
|
||||
? t('copy_tooltip_disabled')
|
||||
: t('copy_tooltip_enabled');
|
||||
const button = (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
className="mcp-settings__copy-btn"
|
||||
icon={<Copy size={14} />}
|
||||
onClick={onCopy}
|
||||
/>
|
||||
);
|
||||
// Ant Design Tooltip doesn't reliably surface for a disabled Button —
|
||||
// wrap in a span so hover/focus still reaches the Tooltip.
|
||||
return (
|
||||
<Tooltip title={tooltipTitle}>
|
||||
{disabled ? <span>{button}</span> : button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
CopyIconButton.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
interface RegionFallbackCardProps {
|
||||
manualRegion: string;
|
||||
onRegionChange: (value: string) => void;
|
||||
onIngestionLinkClick: () => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function RegionFallbackCard({
|
||||
manualRegion,
|
||||
onRegionChange,
|
||||
onIngestionLinkClick,
|
||||
t,
|
||||
}: RegionFallbackCardProps): JSX.Element {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => onRegionChange(e.target.value),
|
||||
[onRegionChange],
|
||||
);
|
||||
return (
|
||||
<div className="mcp-settings__card mcp-settings__region-card">
|
||||
<div className="mcp-settings__region-warning">
|
||||
<TriangleAlert size={14} />
|
||||
<span>
|
||||
{t('region_warning_prefix')}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
className="mcp-settings__inline-link"
|
||||
onClick={onIngestionLinkClick}
|
||||
>
|
||||
{t('region_warning_link')}
|
||||
</Button>
|
||||
{t('region_warning_suffix')}
|
||||
</span>
|
||||
</div>
|
||||
<label
|
||||
className="mcp-settings__auth-field-label"
|
||||
htmlFor="mcp-settings-manual-region"
|
||||
>
|
||||
{t('region_input_label')}
|
||||
</label>
|
||||
<Input
|
||||
id="mcp-settings-manual-region"
|
||||
className="mcp-settings__endpoint-input"
|
||||
size="small"
|
||||
value={manualRegion}
|
||||
placeholder={t('region_input_placeholder')}
|
||||
aria-label={t('region_input_label')}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ClientTabsProps {
|
||||
endpoint: string;
|
||||
activeTab: string;
|
||||
onTabChange: (key: string) => void;
|
||||
onCopySnippet: (clientKey: string, snippet: string) => void;
|
||||
onInstallClick: (clientKey: string) => void;
|
||||
onDocsLinkClick: (target: string) => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
interface ClientTabChildrenProps {
|
||||
client: McpClient;
|
||||
endpoint: string;
|
||||
onCopySnippet: (clientKey: string, snippet: string) => void;
|
||||
onInstallClick: (clientKey: string) => void;
|
||||
onDocsLinkClick: (target: string) => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function ClientTabChildren({
|
||||
client,
|
||||
endpoint,
|
||||
onCopySnippet,
|
||||
onInstallClick,
|
||||
onDocsLinkClick,
|
||||
t,
|
||||
}: ClientTabChildrenProps): JSX.Element {
|
||||
const snippet = client.snippet
|
||||
? client.snippet(endpoint || ENDPOINT_PLACEHOLDER)
|
||||
: null;
|
||||
const installHref =
|
||||
client.installUrl && endpoint ? client.installUrl(endpoint) : null;
|
||||
|
||||
const handleInstallClick = useCallback(() => onInstallClick(client.key), [
|
||||
onInstallClick,
|
||||
client.key,
|
||||
]);
|
||||
const handleDocsClick = useCallback(
|
||||
() => onDocsLinkClick(`client-${client.key}`),
|
||||
[onDocsLinkClick, client.key],
|
||||
);
|
||||
const handleSnippetCopy = useCallback(() => {
|
||||
if (snippet) {
|
||||
onCopySnippet(client.key, snippet);
|
||||
}
|
||||
}, [onCopySnippet, client.key, snippet]);
|
||||
|
||||
const installLabel = client.installLabelKey
|
||||
? t(client.installLabelKey)
|
||||
: `${t('step1_add_to_client_prefix')}${client.label}`;
|
||||
const instructions = client.instructionsKey ? t(client.instructionsKey) : '';
|
||||
|
||||
return (
|
||||
<div className="mcp-settings__snippet-wrapper">
|
||||
{client.installUrl && (
|
||||
<div className="mcp-settings__install-row">
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!installHref}
|
||||
icon={<Download size={14} />}
|
||||
href={installHref ?? undefined}
|
||||
onClick={handleInstallClick}
|
||||
>
|
||||
{installLabel}
|
||||
</Button>
|
||||
<Typography.Text className="mcp-settings__helper-text">
|
||||
{t('step1_manual_fallback')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
{snippet !== null ? (
|
||||
<div className="mcp-settings__endpoint-value mcp-settings__snippet">
|
||||
<pre className="mcp-settings__snippet-pre">{snippet}</pre>
|
||||
<CopyIconButton
|
||||
ariaLabel={`${t('copy_aria_snippet_prefix')}${client.label}${t(
|
||||
'copy_aria_snippet_suffix',
|
||||
)}`}
|
||||
disabled={!endpoint}
|
||||
onCopy={handleSnippetCopy}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Text className="mcp-settings__card-description">
|
||||
{instructions}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<LearnMore
|
||||
text={`${client.label}${t('step1_client_docs_suffix')}`}
|
||||
url={docsUrl(client.docsPath)}
|
||||
onClick={handleDocsClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientTabs({
|
||||
endpoint,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
onCopySnippet,
|
||||
onInstallClick,
|
||||
onDocsLinkClick,
|
||||
t,
|
||||
}: ClientTabsProps): JSX.Element {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
MCP_CLIENTS.map((client: McpClient) => ({
|
||||
key: client.key,
|
||||
label: client.label,
|
||||
children: (
|
||||
<ClientTabChildren
|
||||
client={client}
|
||||
endpoint={endpoint}
|
||||
onCopySnippet={onCopySnippet}
|
||||
onInstallClick={onInstallClick}
|
||||
onDocsLinkClick={onDocsLinkClick}
|
||||
t={t}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[endpoint, onCopySnippet, onInstallClick, onDocsLinkClick, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className="mcp-settings__tabs-container"
|
||||
activeKey={activeTab}
|
||||
onChange={onTabChange}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface AuthCardProps {
|
||||
isAdmin: boolean;
|
||||
instanceUrl: string;
|
||||
onCopyInstanceUrl: () => void;
|
||||
onCreateServiceAccount: () => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function AuthCard({
|
||||
isAdmin,
|
||||
instanceUrl,
|
||||
onCopyInstanceUrl,
|
||||
onCreateServiceAccount,
|
||||
t,
|
||||
}: AuthCardProps): JSX.Element {
|
||||
return (
|
||||
<section className="mcp-settings__card mcp-settings__cta-card">
|
||||
<h3 className="mcp-settings__card-title">
|
||||
<span className="mcp-settings__step-badge">2</span> {t('step2_title')}
|
||||
</h3>
|
||||
<Typography.Text className="mcp-settings__card-description">
|
||||
{t('step2_description')}
|
||||
</Typography.Text>
|
||||
<div className="mcp-settings__auth-field">
|
||||
<Typography.Text className="mcp-settings__auth-field-label">
|
||||
{t('step2_instance_url_label')}
|
||||
</Typography.Text>
|
||||
<div className="mcp-settings__endpoint-value">
|
||||
<span data-testid="mcp-instance-url">{instanceUrl}</span>
|
||||
<CopyIconButton
|
||||
ariaLabel={t('copy_aria_instance_url')}
|
||||
onCopy={onCopyInstanceUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mcp-settings__auth-field">
|
||||
<Typography.Text className="mcp-settings__auth-field-label">
|
||||
{t('step2_api_key_label')}
|
||||
</Typography.Text>
|
||||
{isAdmin ? (
|
||||
<div className="mcp-settings__cta-row">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<KeyRound size={14} />}
|
||||
onClick={onCreateServiceAccount}
|
||||
>
|
||||
{t('step2_admin_cta')}
|
||||
</Button>
|
||||
<Typography.Text className="mcp-settings__helper-text">
|
||||
{t('step2_admin_helper')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mcp-settings__info-banner-inline">
|
||||
<Info size={14} />
|
||||
<Typography.Text className="mcp-settings__helper-text">
|
||||
{t('step2_viewer_helper')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface UseCasesCardProps {
|
||||
onDocsLinkClick: (target: string) => void;
|
||||
t: TFunction<'mcpServer'>;
|
||||
}
|
||||
|
||||
function UseCasesCard({ onDocsLinkClick, t }: UseCasesCardProps): JSX.Element {
|
||||
const handleClick = useCallback(() => onDocsLinkClick('use-cases'), [
|
||||
onDocsLinkClick,
|
||||
]);
|
||||
return (
|
||||
<section className="mcp-settings__card mcp-settings__use-cases">
|
||||
<h3 className="mcp-settings__card-title">{t('use_cases_title')}</h3>
|
||||
<ul className="mcp-settings__use-cases-list">
|
||||
<li>{t('use_cases_item_1')}</li>
|
||||
<li>{t('use_cases_item_2')}</li>
|
||||
<li>{t('use_cases_item_3')}</li>
|
||||
<li>{t('use_cases_item_4')}</li>
|
||||
</ul>
|
||||
<LearnMore
|
||||
text={t('use_cases_docs_link')}
|
||||
url={MCP_USE_CASES_URL}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MCPServerSettings(): JSX.Element {
|
||||
const { t } = useTranslation('mcpServer');
|
||||
const { user } = useAppContext();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
const { notifications } = useNotifications();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const isAdmin = user.role === USER_ROLES.ADMIN;
|
||||
const instanceUrl = window.location.origin;
|
||||
|
||||
const { data: globalConfig } = useGetGlobalConfig();
|
||||
const regionFromHost = useMemo(() => getCloudRegion(), []);
|
||||
const regionFromIngestion = useMemo(
|
||||
() =>
|
||||
globalConfig?.data?.ingestion_url
|
||||
? parseRegionFromUrl(globalConfig.data.ingestion_url)
|
||||
: null,
|
||||
[globalConfig?.data?.ingestion_url],
|
||||
);
|
||||
|
||||
const autoDetectedRegion = regionFromHost.region ?? regionFromIngestion;
|
||||
|
||||
const [manualRegion, setManualRegion] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<string>(MCP_CLIENTS[0]?.key ?? '');
|
||||
|
||||
const resolvedRegion: string | null =
|
||||
autoDetectedRegion ?? normalizeRegion(manualRegion);
|
||||
|
||||
const endpoint = resolvedRegion ? buildMcpEndpoint(resolvedRegion) : '';
|
||||
|
||||
// Fire once on mount so we reliably capture every visit, even if the
|
||||
// globalConfig fetch is slow or fails. Region is best-effort from the
|
||||
// hostname at mount time; if ingestion_url resolves later we skip logging
|
||||
// again to avoid double-fires.
|
||||
useEffect(() => {
|
||||
logEvent(ANALYTICS.PAGE_VIEWED, {
|
||||
isCloudUser,
|
||||
role: user.role,
|
||||
region: regionFromHost.region,
|
||||
isAutoDetected: Boolean(regionFromHost.region),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleCopySnippet = useCallback(
|
||||
(clientKey: string, snippet: string) => {
|
||||
if (!endpoint) {
|
||||
notifications.warning({ message: t('toast_region_required') });
|
||||
return;
|
||||
}
|
||||
copyToClipboard(snippet);
|
||||
notifications.success({ message: t('toast_snippet_copied') });
|
||||
logEvent(ANALYTICS.SNIPPET_COPIED, { client: clientKey });
|
||||
},
|
||||
[endpoint, copyToClipboard, notifications, t],
|
||||
);
|
||||
|
||||
const handleCreateServiceAccount = useCallback(() => {
|
||||
logEvent(ANALYTICS.CREATE_SA_CLICKED, {});
|
||||
history.push(
|
||||
`${ROUTES.SERVICE_ACCOUNTS_SETTINGS}?${SA_QUERY_PARAMS.CREATE_SA}=true`,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleCopyInstanceUrl = useCallback(() => {
|
||||
copyToClipboard(instanceUrl);
|
||||
notifications.success({ message: t('toast_instance_url_copied') });
|
||||
logEvent(ANALYTICS.INSTANCE_URL_COPIED, {});
|
||||
}, [copyToClipboard, instanceUrl, notifications, t]);
|
||||
|
||||
const handleDocsLinkClick = useCallback((target: string) => {
|
||||
logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target });
|
||||
}, []);
|
||||
|
||||
const handleIngestionLinkClick = useCallback(() => {
|
||||
logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target: 'ingestion-settings' });
|
||||
history.push(ROUTES.INGESTION_SETTINGS);
|
||||
}, []);
|
||||
|
||||
const handleInstallClick = useCallback((clientKey: string) => {
|
||||
logEvent(ANALYTICS.ONE_CLICK_INSTALL_CLICKED, { client: clientKey });
|
||||
}, []);
|
||||
|
||||
const handleTabChange = useCallback((key: string) => {
|
||||
setActiveTab(key);
|
||||
logEvent(ANALYTICS.CLIENT_TAB_SELECTED, { client: key });
|
||||
}, []);
|
||||
|
||||
if (!isCloudUser) {
|
||||
return <NotCloudFallback />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mcp-settings" data-testid="mcp-settings">
|
||||
<header className="mcp-settings__header">
|
||||
<h2 className="mcp-settings__header-title">
|
||||
<Sparkles size={20} /> {t('page_title')}
|
||||
</h2>
|
||||
<Typography.Text className="mcp-settings__header-subtitle">
|
||||
{t('page_subtitle')}
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
{!autoDetectedRegion && (
|
||||
<RegionFallbackCard
|
||||
manualRegion={manualRegion}
|
||||
onRegionChange={setManualRegion}
|
||||
onIngestionLinkClick={handleIngestionLinkClick}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
<section className="mcp-settings__card">
|
||||
<h3 className="mcp-settings__card-title">
|
||||
<span className="mcp-settings__step-badge">1</span> {t('step1_title')}
|
||||
</h3>
|
||||
<Typography.Text className="mcp-settings__card-description">
|
||||
{t('step1_description')}
|
||||
</Typography.Text>
|
||||
<ClientTabs
|
||||
endpoint={endpoint}
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
onCopySnippet={handleCopySnippet}
|
||||
onInstallClick={handleInstallClick}
|
||||
onDocsLinkClick={handleDocsLinkClick}
|
||||
t={t}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AuthCard
|
||||
isAdmin={isAdmin}
|
||||
instanceUrl={instanceUrl}
|
||||
onCopyInstanceUrl={handleCopyInstanceUrl}
|
||||
onCreateServiceAccount={handleCreateServiceAccount}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<UseCasesCard onDocsLinkClick={handleDocsLinkClick} t={t} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MCPServerSettings;
|
||||
@@ -1,111 +0,0 @@
|
||||
import { DOCS_BASE_URL } from 'constants/app';
|
||||
|
||||
export interface McpClient {
|
||||
key: string;
|
||||
// `label` is the client brand name (Cursor, VS Code, Claude Desktop …).
|
||||
// Brand names are not translated.
|
||||
label: string;
|
||||
docsPath: string;
|
||||
snippet: ((endpoint: string) => string) | null;
|
||||
// i18n key under the `mcpServer` namespace. Resolved at render time via t().
|
||||
instructionsKey?: string;
|
||||
installUrl?: (endpoint: string) => string;
|
||||
// i18n key for the install button label. Falls back to
|
||||
// `step1_add_to_client_prefix` + `label` when not set.
|
||||
installLabelKey?: string;
|
||||
}
|
||||
|
||||
function b64url(input: string): string {
|
||||
if (typeof btoa === 'function') {
|
||||
return btoa(input);
|
||||
}
|
||||
// fallback for non-browser TS contexts (never hit at runtime)
|
||||
return Buffer.from(input, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
export const MCP_CLIENTS: McpClient[] = [
|
||||
{
|
||||
key: 'cursor',
|
||||
label: 'Cursor',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#cursor',
|
||||
snippet: (endpoint): string =>
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
signoz: {
|
||||
url: endpoint,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
installUrl: (endpoint): string => {
|
||||
const config = b64url(JSON.stringify({ url: endpoint }));
|
||||
return `cursor://anysphere.cursor-deeplink/mcp/install?name=SigNoz&config=${config}`;
|
||||
},
|
||||
installLabelKey: 'client_cursor_install_label',
|
||||
},
|
||||
{
|
||||
key: 'vscode',
|
||||
label: 'VS Code',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#vs-code',
|
||||
snippet: (endpoint): string =>
|
||||
JSON.stringify(
|
||||
{
|
||||
servers: {
|
||||
signoz: {
|
||||
type: 'http',
|
||||
url: endpoint,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
installUrl: (endpoint): string => {
|
||||
const payload = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
name: 'signoz',
|
||||
config: { type: 'http', url: endpoint },
|
||||
}),
|
||||
);
|
||||
return `vscode:mcp/install?${payload}`;
|
||||
},
|
||||
installLabelKey: 'client_vscode_install_label',
|
||||
},
|
||||
{
|
||||
key: 'claude-desktop',
|
||||
label: 'Claude Desktop',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#claude-desktop',
|
||||
snippet: null,
|
||||
instructionsKey: 'client_claude_desktop_instructions',
|
||||
},
|
||||
{
|
||||
key: 'claude-code',
|
||||
label: 'Claude Code',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#claude-code',
|
||||
snippet: (endpoint): string =>
|
||||
`claude mcp add --scope user --transport http signoz ${endpoint}`,
|
||||
},
|
||||
{
|
||||
key: 'codex',
|
||||
label: 'Codex',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/#codex',
|
||||
snippet: (endpoint): string => `codex mcp add signoz --url ${endpoint}`,
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
label: 'Other',
|
||||
docsPath: '/docs/ai/signoz-mcp-server/',
|
||||
snippet: null,
|
||||
instructionsKey: 'client_other_instructions',
|
||||
},
|
||||
];
|
||||
|
||||
export function docsUrl(path: string): string {
|
||||
return `${DOCS_BASE_URL}${path}`;
|
||||
}
|
||||
|
||||
export const MCP_DOCS_URL = `${DOCS_BASE_URL}/docs/ai/signoz-mcp-server/`;
|
||||
export const MCP_USE_CASES_URL = `${DOCS_BASE_URL}/docs/ai/use-cases/`;
|
||||
@@ -1,51 +0,0 @@
|
||||
export interface CloudRegionResult {
|
||||
region: string | null;
|
||||
isKnown: boolean;
|
||||
}
|
||||
|
||||
const VALID_REGION_LABEL = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
export function parseRegionFromSignozCloudHost(host: string): string | null {
|
||||
const parts = host.split('.');
|
||||
const len = parts.length;
|
||||
// SigNoz Cloud tenant hosts follow `<tenant>.<region>.signoz.cloud`
|
||||
// (4 labels). 3-label hosts like `app.signoz.cloud` would wrongly
|
||||
// resolve to region=`app`, so require at least 4 labels.
|
||||
if (len < 4 || parts[len - 1] !== 'cloud' || parts[len - 2] !== 'signoz') {
|
||||
return null;
|
||||
}
|
||||
const region = parts[len - 3]?.toLowerCase() ?? '';
|
||||
if (!VALID_REGION_LABEL.test(region)) {
|
||||
return null;
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
export function parseRegionFromUrl(url: string): string | null {
|
||||
try {
|
||||
return parseRegionFromSignozCloudHost(new URL(url).hostname);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCloudRegion(hostname: string): CloudRegionResult {
|
||||
const region = parseRegionFromSignozCloudHost(hostname);
|
||||
return region ? { region, isKnown: true } : { region: null, isKnown: false };
|
||||
}
|
||||
|
||||
export function normalizeRegion(input: string): string | null {
|
||||
const value = input.trim().toLowerCase();
|
||||
if (!VALID_REGION_LABEL.test(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function buildMcpEndpoint(region: string): string {
|
||||
return `https://mcp.${region}.signoz.cloud/mcp`;
|
||||
}
|
||||
|
||||
export function getCloudRegion(): CloudRegionResult {
|
||||
return parseCloudRegion(window.location.hostname);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
import { TopOperationList } from './TopOperationsTable';
|
||||
import { NavigateToTraceProps } from './types';
|
||||
@@ -37,7 +38,7 @@ export const navigateToTrace = ({
|
||||
}=${JSONCompositeQuery}`;
|
||||
|
||||
if (openInNewTab) {
|
||||
window.open(newTraceExplorerPath, '_blank');
|
||||
window.open(withBasePath(newTraceExplorerPath), '_blank');
|
||||
} 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,10 @@ function DashboardsAndAlertsPopover({
|
||||
<Typography.Link
|
||||
key={dashboard.dashboardId}
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
openInNewTab(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: dashboard.dashboardId,
|
||||
}),
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import useContextVariables from 'hooks/dashboard/useContextVariables';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
import VariablesDropdown from './VariablesDropdown';
|
||||
|
||||
@@ -84,7 +85,7 @@ function UpdateContextLinks({
|
||||
);
|
||||
|
||||
// Function to get current domain
|
||||
const getCurrentDomain = (): string => window.location.origin;
|
||||
const getCurrentDomain = (): string => getBaseUrl();
|
||||
|
||||
// Function to handle variable selection from dropdown
|
||||
const handleVariableSelect = (
|
||||
|
||||
@@ -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 { openInNewTab } 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');
|
||||
openInNewTab(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE);
|
||||
} else if (dataSource === DataSource.METRICS) {
|
||||
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
|
||||
openInNewTab(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE);
|
||||
} else {
|
||||
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
|
||||
openInNewTab(`${DOCLINKS.USER_GUIDE}${dataSource}/`);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
|
||||
@@ -60,7 +61,7 @@ function InviteTeamMembers({
|
||||
email: '',
|
||||
role: '',
|
||||
name: '',
|
||||
frontendBaseUrl: window.location.origin,
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
id: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DOCS_BASE_URL } from 'constants/app';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowUpRight, Copy, Info, Key, TriangleAlert } from 'lucide-react';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
import './IngestionDetails.styles.scss';
|
||||
|
||||
@@ -215,7 +216,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
</a>
|
||||
. To create a new one,{' '}
|
||||
<a
|
||||
href="/settings/ingestion-settings"
|
||||
href={withBasePath('/settings/ingestion-settings')}
|
||||
target="_blank"
|
||||
className="learn-more"
|
||||
rel="noreferrer"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
|
||||
import { ArrowRight, CheckCircle, Plus, TriangleAlert, X } from 'lucide-react';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import './InviteTeamMembers.styles.scss';
|
||||
@@ -56,7 +57,7 @@ function InviteTeamMembers({
|
||||
email: '',
|
||||
role: 'EDITOR',
|
||||
name: '',
|
||||
frontendBaseUrl: window.location.origin,
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
id: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -192,8 +192,7 @@ const onboardingConfigWithLinks = [
|
||||
'setup',
|
||||
],
|
||||
imgUrl: signozBrandLogoUrl,
|
||||
link: '/settings/mcp-server',
|
||||
internalRedirect: true,
|
||||
link: '/docs/ai/signoz-mcp-server/',
|
||||
},
|
||||
{
|
||||
dataSource: 'migrate-from-datadog',
|
||||
|
||||
@@ -18,6 +18,7 @@ import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
import CreateEdit from './CreateEdit/CreateEdit';
|
||||
import SSOEnforcementToggle from './SSOEnforcementToggle';
|
||||
@@ -145,7 +146,7 @@ function AuthDomain(): JSX.Element {
|
||||
return <span className="auth-domain-list-na">N/A</span>;
|
||||
}
|
||||
|
||||
const href = `${window.location.origin}/${relayPath}`;
|
||||
const href = getAbsoluteUrl(`/${relayPath}`);
|
||||
return <CopyToClipboard textToCopy={href} />;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Form, FormInstance, Modal } from 'antd';
|
||||
import sendInvite from 'api/v1/invite/create';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
import InviteTeamMembers from '../InviteTeamMembers';
|
||||
import { InviteMemberFormValues } from '../utils';
|
||||
@@ -40,7 +41,7 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
||||
email: member.email,
|
||||
name: member?.name,
|
||||
role: member.role,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
});
|
||||
|
||||
notifications.success({
|
||||
|
||||
@@ -14,6 +14,7 @@ import ContextMenu from 'periscope/components/ContextMenu';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ContextLinksData } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { ContextMenuItem } from './contextConfig';
|
||||
import { getDataLinks } from './dataLinksUtils';
|
||||
@@ -115,7 +116,7 @@ const useBaseAggregateOptions = ({
|
||||
key={id}
|
||||
icon={<LinkOutlined />}
|
||||
onClick={(): void => {
|
||||
window.open(url, '_blank');
|
||||
openInNewTab(url);
|
||||
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.
|
||||
|
||||
@@ -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');
|
||||
openInNewTab(item.url);
|
||||
}
|
||||
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
Settings,
|
||||
Shield,
|
||||
Slack,
|
||||
Sparkles,
|
||||
Unplug,
|
||||
User,
|
||||
UserPlus,
|
||||
@@ -338,13 +337,6 @@ export const settingsNavSections: SettingsNavSection[] = [
|
||||
isEnabled: false,
|
||||
itemKey: 'integrations',
|
||||
},
|
||||
{
|
||||
key: ROUTES.MCP_SERVER,
|
||||
label: 'MCP Server',
|
||||
icon: <Sparkles size={16} />,
|
||||
isEnabled: false,
|
||||
itemKey: 'mcp-server',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -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 { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
import { RelatedSignalsViews } from '../constants';
|
||||
import SpanLogs from '../SpanLogs/SpanLogs';
|
||||
@@ -158,9 +159,7 @@ function SpanRelatedSignals({
|
||||
searchParams.set(QueryParams.endTime, endTimeMs.toString());
|
||||
|
||||
window.open(
|
||||
`${window.location.origin}${
|
||||
ROUTES.LOGS_EXPLORER
|
||||
}?${searchParams.toString()}`,
|
||||
getAbsoluteUrl(`${ROUTES.LOGS_EXPLORER}?${searchParams.toString()}`),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
|
||||
@@ -157,7 +157,6 @@ export const routesToSkip = [
|
||||
ROUTES.ORG_SETTINGS,
|
||||
ROUTES.MEMBERS_SETTINGS,
|
||||
ROUTES.SERVICE_ACCOUNTS_SETTINGS,
|
||||
ROUTES.MCP_SERVER,
|
||||
ROUTES.INGESTION_SETTINGS,
|
||||
ROUTES.ERROR_DETAIL,
|
||||
ROUTES.LOGS_PIPELINES,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import setSessionStorageApi from 'api/browser/sessionstorage/set';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
@@ -61,7 +62,7 @@ function useDashboardsListQueryParams(): {
|
||||
|
||||
const queryParamsString = params.toString();
|
||||
|
||||
sessionStorage.setItem(
|
||||
setSessionStorageApi(
|
||||
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
|
||||
queryParamsString,
|
||||
);
|
||||
|
||||
@@ -172,6 +172,7 @@ export function useIntegrationModal({
|
||||
id: accountId,
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line rulesdir/no-raw-absolute-path -- connectionUrl is an external AWS console URL, not an internal path
|
||||
window.open(connectionUrl, '_blank');
|
||||
setModalState(ModalStateEnum.WAITING);
|
||||
setAccountId(accountId);
|
||||
|
||||
@@ -17,6 +17,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
import { HIGHLIGHTED_DELAY } from './configs';
|
||||
import { UseCopyLogLink } from './types';
|
||||
@@ -60,7 +61,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
|
||||
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
|
||||
|
||||
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
|
||||
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
|
||||
|
||||
setCopy(link);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
|
||||
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
@@ -92,7 +93,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
|
||||
const url = `${ROUTES.ALERTS_NEW}?${params.toString()}`;
|
||||
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
window.open(withBasePath(url), '_blank', 'noreferrer');
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCopyToClipboard } from 'react-use';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
export const useCopySpanLink = (
|
||||
span?: Span,
|
||||
@@ -28,7 +29,7 @@ export const useCopySpanLink = (
|
||||
urlQuery.set('spanId', span?.spanId);
|
||||
}
|
||||
|
||||
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
|
||||
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
|
||||
|
||||
setCopy(link);
|
||||
notifications.success({
|
||||
|
||||
@@ -1,34 +1,20 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import removeLocalStorageApi from 'api/browser/localstorage/remove';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
|
||||
/**
|
||||
* A React hook for interacting with localStorage.
|
||||
* It allows getting, setting, and removing items from localStorage.
|
||||
*
|
||||
* @template T The type of the value to be stored.
|
||||
* @param {string} key The localStorage key.
|
||||
* @param {T | (() => T)} defaultValue The default value to use if no value is found in localStorage,
|
||||
* @returns {[T, (value: T | ((prevState: T) => T)) => void, () => void]}
|
||||
* A tuple containing:
|
||||
* - The current value from state (and localStorage).
|
||||
* - A function to set the value (updates state and localStorage).
|
||||
* - A function to remove the value from localStorage and reset state to defaultValue.
|
||||
*/
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
defaultValue: T | (() => T),
|
||||
): [T, (value: T | ((prevState: T) => T)) => void, () => void] {
|
||||
// Stabilize the defaultValue to prevent unnecessary re-renders
|
||||
const defaultValueRef = useRef<T | (() => T)>(defaultValue);
|
||||
|
||||
// Update the ref if defaultValue changes (for cases where it's intentionally dynamic)
|
||||
useEffect(() => {
|
||||
if (defaultValueRef.current !== defaultValue) {
|
||||
defaultValueRef.current = defaultValue;
|
||||
}
|
||||
}, [defaultValue]);
|
||||
|
||||
// This function resolves the defaultValue if it's a function,
|
||||
// and handles potential errors during localStorage access or JSON parsing.
|
||||
const readValueFromStorage = useCallback((): T => {
|
||||
const resolveddefaultValue =
|
||||
defaultValueRef.current instanceof Function
|
||||
@@ -36,33 +22,25 @@ export function useLocalStorage<T>(
|
||||
: defaultValueRef.current;
|
||||
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
// If item exists, parse it, otherwise return the resolved default value.
|
||||
const item = getLocalStorageApi(key);
|
||||
if (item) {
|
||||
return JSON.parse(item) as T;
|
||||
}
|
||||
} catch (error) {
|
||||
// Log error and fall back to default value if reading/parsing fails.
|
||||
console.warn(`Error reading localStorage key "${key}":`, error);
|
||||
}
|
||||
return resolveddefaultValue;
|
||||
}, [key]);
|
||||
|
||||
// Initialize state by reading from localStorage.
|
||||
const [storedValue, setStoredValue] = useState<T>(readValueFromStorage);
|
||||
|
||||
// This function updates both localStorage and the React state.
|
||||
const setValue = useCallback(
|
||||
(value: T | ((prevState: T) => T)) => {
|
||||
try {
|
||||
// If a function is passed to setValue, it receives the latest value from storage.
|
||||
const latestValueFromStorage = readValueFromStorage();
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(latestValueFromStorage) : value;
|
||||
|
||||
// Save to localStorage.
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
// Update React state.
|
||||
setLocalStorageApi(key, JSON.stringify(valueToStore));
|
||||
setStoredValue(valueToStore);
|
||||
} catch (error) {
|
||||
console.warn(`Error setting localStorage key "${key}":`, error);
|
||||
@@ -71,11 +49,9 @@ export function useLocalStorage<T>(
|
||||
[key, readValueFromStorage],
|
||||
);
|
||||
|
||||
// This function removes the item from localStorage and resets the React state.
|
||||
const removeValue = useCallback(() => {
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
// Reset state to the (potentially resolved) defaultValue.
|
||||
removeLocalStorageApi(key);
|
||||
setStoredValue(
|
||||
defaultValueRef.current instanceof Function
|
||||
? (defaultValueRef.current as () => T)()
|
||||
@@ -86,12 +62,9 @@ export function useLocalStorage<T>(
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
// useEffect to update the storedValue if the key changes,
|
||||
// or if the defaultValue prop changes causing readValueFromStorage to change.
|
||||
// This ensures the hook reflects the correct localStorage item if its key prop dynamically changes.
|
||||
useEffect(() => {
|
||||
setStoredValue(readValueFromStorage());
|
||||
}, [key, readValueFromStorage]); // Re-run if key or the read function changes.
|
||||
}, [key, readValueFromStorage]);
|
||||
|
||||
return [storedValue, setValue, removeValue];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
interface NavigateOptions {
|
||||
replace?: boolean;
|
||||
@@ -107,19 +108,18 @@ export const useSafeNavigate = (
|
||||
const safeNavigate = useCallback(
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
|
||||
const currentUrl = new URL(
|
||||
`${location.pathname}${location.search}`,
|
||||
window.location.origin,
|
||||
);
|
||||
// eslint-disable-next-line rulesdir/no-raw-absolute-path
|
||||
const base = window.location.origin;
|
||||
const currentUrl = new URL(`${location.pathname}${location.search}`, base);
|
||||
|
||||
let targetUrl: URL;
|
||||
|
||||
if (typeof to === 'string') {
|
||||
targetUrl = new URL(to, window.location.origin);
|
||||
targetUrl = new URL(to, base);
|
||||
} else {
|
||||
targetUrl = new URL(
|
||||
`${to.pathname || location.pathname}${to.search || ''}`,
|
||||
window.location.origin,
|
||||
base,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ export const useSafeNavigate = (
|
||||
typeof to === 'string'
|
||||
? to
|
||||
: `${to.pathname || location.pathname}${to.search || ''}`;
|
||||
window.open(targetPath, '_blank');
|
||||
window.open(withBasePath(targetPath), '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
|
||||
export default createBrowserHistory();
|
||||
export default createBrowserHistory({ basename: getBasePath() });
|
||||
|
||||
@@ -4,6 +4,7 @@ import ROUTES from 'constants/routes';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { Home, LifeBuoy } from 'lucide-react';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
import cloudUrl from '@/assets/Images/cloud.svg';
|
||||
|
||||
@@ -11,8 +12,8 @@ import './ErrorBoundaryFallback.styles.scss';
|
||||
|
||||
function ErrorBoundaryFallback(): JSX.Element {
|
||||
const handleReload = (): void => {
|
||||
// Go to home page
|
||||
window.location.href = ROUTES.HOME;
|
||||
// Hard reload resets Sentry.ErrorBoundary state; withBasePath preserves any /signoz/ prefix.
|
||||
window.location.href = withBasePath(ROUTES.HOME);
|
||||
};
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -59,7 +59,7 @@ function MessagingQueues(): JSX.Element {
|
||||
history.push(link);
|
||||
}
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
openInNewTab(KAFKA_SETUP_DOC_LINK);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -75,11 +75,6 @@ function SettingsPage(): JSX.Element {
|
||||
}
|
||||
|
||||
if (isCloudUser) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
isEnabled: item.key === ROUTES.MCP_SERVER ? true : item.isEnabled,
|
||||
}));
|
||||
|
||||
if (isAdmin) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
|
||||
@@ -8,7 +8,6 @@ import GeneralSettings from 'container/GeneralSettings';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
|
||||
import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings';
|
||||
import MCPServerSettings from 'container/MCPServerSettings/MCPServerSettings';
|
||||
import MySettings from 'container/MySettings';
|
||||
import OrganizationSettings from 'container/OrganizationSettings';
|
||||
import RolesSettings from 'container/RolesSettings';
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
Pencil,
|
||||
Plus,
|
||||
Shield,
|
||||
Sparkles,
|
||||
User,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
@@ -207,19 +205,6 @@ export const serviceAccountsSettings = (
|
||||
},
|
||||
];
|
||||
|
||||
export const mcpServerSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: MCPServerSettings,
|
||||
name: (
|
||||
<div className="periscope-tab">
|
||||
<Sparkles size={16} /> {t('routes:mcp_server').toString()}
|
||||
</div>
|
||||
),
|
||||
route: ROUTES.MCP_SERVER,
|
||||
key: ROUTES.MCP_SERVER,
|
||||
},
|
||||
];
|
||||
|
||||
export const createAlertChannels = (t: TFunction): RouteTabProps['routes'] => [
|
||||
{
|
||||
Component: (): JSX.Element => (
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
generalSettings,
|
||||
ingestionSettings,
|
||||
keyboardShortcuts,
|
||||
mcpServerSettings,
|
||||
membersSettings,
|
||||
multiIngestionSettings,
|
||||
mySettings,
|
||||
@@ -80,10 +79,6 @@ export const getRoutes = (
|
||||
...createAlertChannels(t),
|
||||
...editAlertChannels(t),
|
||||
...keyboardShortcuts(t),
|
||||
// Route is registered for everyone so direct-URL visitors see the
|
||||
// in-page fallback. Sidebar visibility is still Cloud-only, gated in
|
||||
// Settings.tsx.
|
||||
...mcpServerSettings(t),
|
||||
);
|
||||
|
||||
return settings;
|
||||
|
||||
@@ -20,6 +20,8 @@ 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 { getBaseUrl } from 'utils/basePath';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import './Support.styles.scss';
|
||||
|
||||
@@ -92,7 +94,7 @@ export default function Support(): JSX.Element {
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const handleChannelWithRedirects = (url: string): void => {
|
||||
window.open(url, '_blank');
|
||||
openInNewTab(url);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -150,7 +152,7 @@ export default function Support(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user