Compare commits

..

34 Commits

Author SHA1 Message Date
SagarRajput-7
3a5d3225dd feat(base-path): migrate backend bound urls and eslint upgrade to error 2026-04-20 15:42:10 +05:30
SagarRajput-7
b61376b904 feat(base-path): migrate remaining pattern for window.location.origin + path 2026-04-20 14:18:59 +05:30
SagarRajput-7
18429b3b91 feat(base-path): replace window.open with openInNewTab for internal paths 2026-04-20 14:05:16 +05:30
SagarRajput-7
c7adb22c61 feat: code refactor around feedbacks 2026-04-20 13:34:46 +05:30
SagarRajput-7
b936e63658 feat: applied suggested patch changes 2026-04-20 12:14:15 +05:30
SagarRajput-7
727b105b6c Merge branch 'main' into base-path-config-1 2026-04-20 12:02:01 +05:30
SagarRajput-7
f3269318b7 feat: code refactor around feedbacks 2026-04-17 15:58:00 +05:30
SagarRajput-7
5088bd7499 feat: updated base path utils and fixed navigation and translations 2026-04-17 13:52:12 +05:30
SagarRajput-7
7648d4f3d3 feat: updated the html template 2026-04-16 21:55:21 +05:30
SagarRajput-7
5729a4584a feat: removed plugin and serving the index.html only as the template 2026-04-16 18:23:46 +05:30
SagarRajput-7
825d06249d feat: refactor the interceptor and added gotmpl into gitignore 2026-04-16 18:23:38 +05:30
SagarRajput-7
9034471587 feat: changed output path to dir level 2026-04-16 18:23:25 +05:30
SagarRajput-7
4cc23ead6b feat: base path config setup and plugin for gotmpl generation at build time 2026-04-16 18:19:07 +05:30
SagarRajput-7
867e27d45f Merge branch 'main' into platform-pod/issues/1775 2026-04-16 18:17:11 +05:30
grandwizard28
be37e588f8 perf(web): cache http.FileServer on provider instead of creating per-request 2026-04-16 14:53:06 +05:30
grandwizard28
057dcbe6e4 fix: remove unused files 2026-04-16 02:26:18 +05:30
grandwizard28
3a28d741a3 fix: remove unused files 2026-04-16 02:24:20 +05:30
grandwizard28
223e83154f style: formatting and test cleanup from review
Restructure Validate nil check, rename expectErr to fail with
early-return, trim trailing newlines in test assertions, remove
t.Parallel from subtests, inline short config literals, restore
struct field comments in web.Config.
2026-04-16 02:17:14 +05:30
grandwizard28
50ae51cdaa fix(web): resolve lint errors in provider and template
Fix errcheck on rw.Write in serveIndex, use ErrorContext instead of
Error in NewIndex for sloglint compliance. Move serveIndex below
ServeHTTP to order public methods before private ones.
2026-04-16 02:05:25 +05:30
grandwizard28
c8ae8476c3 style: add blank lines between logical blocks 2026-04-16 01:57:24 +05:30
grandwizard28
daaa66e1fc chore: remove redundant comments from added code 2026-04-16 01:54:14 +05:30
grandwizard28
b0717d6a69 refactor(web): use table-driven tests with named path cases
Replace for-loop path iteration with explicit table-driven test cases
for each path. Each path (root, non-existent, directory) is a named
subtest case in all three template tests.
2026-04-16 01:49:07 +05:30
grandwizard28
4aefe44313 refactor(web): rename get test helper to httpGet 2026-04-16 01:47:35 +05:30
grandwizard28
4dc6f6fe7b style(web): use raw string literals for expected test values 2026-04-16 01:44:46 +05:30
grandwizard28
d3e0c46ba2 test(web): use exact match instead of contains in template tests
Match the full expected response body in TestServeTemplatedIndex
instead of using assert.Contains.
2026-04-16 01:43:23 +05:30
grandwizard28
0fed17e11a test(web): add SPA fallback paths to no_template and invalid_template tests
Test /, /does-not-exist, and /assets in all three template test cases
to verify SPA fallback behavior (non-existent paths and directories
serve the index) regardless of template type.
2026-04-16 01:38:46 +05:30
grandwizard28
a2264b4960 refactor(web): rename test fixtures to no_template, valid_template, invalid_template
Drop the index_ prefix from test fixtures. Use web instead of w for
the variable name in test helpers.
2026-04-16 01:32:50 +05:30
grandwizard28
2740964106 test(web): add no-template and invalid-template index test cases
Add three distinct index fixtures in testdata:
- index.html: correct [[ ]] template with BaseHref
- index_no_template.html: plain HTML, no placeholders
- index_invalid_template.html: malformed template syntax

Tests verify: template substitution works, plain files pass through
unchanged, and invalid templates fall back to serving raw bytes.
Consolidate test helpers into startServer/get.
2026-04-16 01:28:37 +05:30
grandwizard28
0ca22dd7fe refactor(web): collapse testdata_basepath into testdata
Use a single testdata directory with a templated index.html for all
routerweb tests. Remove the redundant testdata_basepath directory.
2026-04-16 01:22:54 +05:30
grandwizard28
a3b6bddac8 refactor(web): make index filename configurable via web.index
Move the hardcoded indexFileName const from routerweb/provider.go to
web.Config.Index with default "index.html". This allows overriding the
SPA entrypoint file via configuration.
2026-04-16 01:19:35 +05:30
grandwizard28
d908ce321a refactor(global): rename RoutePrefix to ExternalPath, add ExternalPathTrailing
Rename RoutePrefix() to ExternalPath() to accurately reflect what it
returns: the path component of the external URL. Add
ExternalPathTrailing() which returns the path with a trailing slash,
used for HTML base href injection.
2026-04-16 01:13:16 +05:30
grandwizard28
c221a44f3d refactor(web): extract index.html templating into web.NewIndex
Move the template parsing and execution logic from routerweb provider
into pkg/web/template.go. NewIndex logs and returns raw bytes on
template failure; NewIndexE returns the error for callers that need it.

Rename BasePath to BaseHref to match the HTML attribute it populates.
Inject global.Config into routerweb via the factory closure pattern.
2026-04-16 01:08:46 +05:30
grandwizard28
22fb4daaf9 feat(web): template index.html with dynamic base href from global.external_url
Read index.html at startup, parse as Go template with [[ ]] delimiters,
execute with BasePath derived from global.external_url, and cache the
rendered bytes in memory. This injects <base href="/signoz/" /> (or
whatever the route prefix is) so the browser resolves relative URLs
correctly when SigNoz is served at a sub-path.

Inject global.Config into the routerweb provider via the factory closure
pattern. Static files (JS, CSS, images) are still served from disk
unchanged.
2026-04-16 00:58:20 +05:30
grandwizard28
1bdc059d76 feat(apiserver): derive HTTP route prefix from global.external_url
The path component of global.external_url is now used as the base path
for all HTTP routes (API and web frontend), enabling SigNoz to be served
behind a reverse proxy at a sub-path (e.g. https://example.com/signoz/).

The prefix is applied via http.StripPrefix at the outermost handler
level, requiring zero changes to route registration code. Health
endpoints (/api/v1/health, /api/v2/healthz, /api/v2/readyz,
/api/v2/livez) remain accessible without the prefix for container
healthchecks.

Removes web.prefix config in favor of the unified global.external_url
approach, avoiding the desync bugs seen in projects with separate
API/UI prefix configs (ArgoCD, Prometheus).

closes SigNoz/platform-pod#1775
2026-04-16 00:38:55 +05:30
132 changed files with 1864 additions and 1161 deletions

View File

@@ -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
@@ -246,6 +248,8 @@ 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',
},
},
{

View 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' });
},
};
},
};

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/create';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.post('/rules', {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default create;

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
AlertRuleV2,
PostableAlertRuleV2,
} from 'types/api/alerts/alertTypesV2';
export interface CreateAlertRuleResponse {
data: AlertRuleV2;
status: string;
}
const createAlertRule = async (
props: PostableAlertRuleV2,
): Promise<SuccessResponse<CreateAlertRuleResponse> | ErrorResponse> => {
const response = await axios.post(`/rules`, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default createAlertRule;

View File

@@ -0,0 +1,18 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/delete';
const deleteAlerts = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.delete(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
};
export default deleteAlerts;

View File

@@ -0,0 +1,16 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/get';
const get = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.get(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default get;

View File

@@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/alerts/getAll';
const getAll = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get('/rules');
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getAll;

View File

@@ -0,0 +1,29 @@
import { AxiosAlertManagerInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import convertObjectIntoParams from 'lib/query/convertObjectIntoParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/getGroups';
const getGroups = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const queryParams = convertObjectIntoParams(props);
const response = await AxiosAlertManagerInstance.get(
`/alerts/groups?${queryParams}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getGroups;

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/patch';
const patch = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.patch(`/rules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default patch;

View File

@@ -1,12 +0,0 @@
import { patchRuleByID } from 'api/generated/services/rules';
import type { RuletypesPostableRuleDTO } from 'api/generated/services/sigNoz.schemas';
// why: patchRuleByID's generated body type is the full RuletypesPostableRuleDTO
// because the backend OpenAPI spec currently advertises PostableRule. The
// endpoint itself accepts any subset of fields. Until the backend introduces
// PatchableRule, this wrapper localizes the cast so callers stay typed.
export const patchRulePartial = (
id: string,
patch: Partial<RuletypesPostableRuleDTO>,
): ReturnType<typeof patchRuleByID> =>
patchRuleByID({ id }, patch as RuletypesPostableRuleDTO);

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/save';
const put = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.put(`/rules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default put;

View File

@@ -0,0 +1,18 @@
import { isEmpty } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/save';
import create from './create';
import put from './put';
const save = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id && !isEmpty(props.id)) {
return put({ ...props });
}
return create({ ...props });
};
export default save;

View File

@@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/testAlert';
const testAlert = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/testRule', {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default testAlert;

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
export interface TestAlertRuleResponse {
data: {
alertCount: number;
message: string;
};
status: string;
}
const testAlertRule = async (
props: PostableAlertRuleV2,
): Promise<SuccessResponse<TestAlertRuleResponse> | ErrorResponse> => {
const response = await axios.post(`/testRule`, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default testAlertRule;

View File

@@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
export interface UpdateAlertRuleResponse {
data: string;
status: string;
}
const updateAlertRule = async (
id: string,
postableAlertRule: PostableAlertRuleV2,
): Promise<SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse> => {
const response = await axios.put(`/rules/${id}`, {
...postableAlertRule,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateAlertRule;

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { Dayjs } from 'dayjs';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Recurrence } from './getAllDowntimeSchedules';
export interface DowntimeSchedulePayload {
name: string;
description?: string;
alertIds: string[];
schedule: {
timezone?: string;
startTime?: string | Dayjs;
endTime?: string | Dayjs;
recurrence?: Recurrence;
};
}
export interface PayloadProps {
status: string;
data: string;
}
const createDowntimeSchedule = async (
props: DowntimeSchedulePayload,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/downtime_schedules', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default createDowntimeSchedule;

View File

@@ -0,0 +1,19 @@
import { useMutation, UseMutationResult } from 'react-query';
import axios from 'api';
export interface DeleteDowntimeScheduleProps {
id?: number;
}
export interface DeleteSchedulePayloadProps {
status: string;
data: string;
}
export const useDeleteDowntimeSchedule = (
props: DeleteDowntimeScheduleProps,
): UseMutationResult<DeleteSchedulePayloadProps, Error, number> =>
useMutation({
mutationKey: [props.id],
mutationFn: () => axios.delete(`/downtime_schedules/${props.id}`),
});

View File

@@ -0,0 +1,51 @@
import { useQuery, UseQueryResult } from 'react-query';
import axios from 'api';
import { AxiosError, AxiosResponse } from 'axios';
import { Option } from 'container/PlannedDowntime/PlannedDowntimeutils';
export type Recurrence = {
startTime?: string | null;
endTime?: string | null;
duration?: number | string | null;
repeatType?: string | Option | null;
repeatOn?: string[] | null;
};
type Schedule = {
timezone: string | null;
startTime: string | null;
endTime: string | null;
recurrence: Recurrence | null;
};
export interface DowntimeSchedules {
id: number;
name: string | null;
description: string | null;
schedule: Schedule | null;
alertIds: string[] | null;
createdAt: string | null;
createdBy: string | null;
updatedAt: string | null;
updatedBy: string | null;
kind: string | null;
}
export type PayloadProps = { data: DowntimeSchedules[] };
export const getAllDowntimeSchedules = async (
props?: GetAllDowntimeSchedulesPayloadProps,
): Promise<AxiosResponse<PayloadProps>> =>
axios.get('/downtime_schedules', { params: props });
export interface GetAllDowntimeSchedulesPayloadProps {
active?: boolean;
recurrence?: boolean;
}
export const useGetAllDowntimeSchedules = (
props?: GetAllDowntimeSchedulesPayloadProps,
): UseQueryResult<AxiosResponse<PayloadProps>, AxiosError> =>
useQuery<AxiosResponse<PayloadProps>, AxiosError>({
queryKey: ['getAllDowntimeSchedules', props],
queryFn: () => getAllDowntimeSchedules(props),
});

View File

@@ -0,0 +1,37 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DowntimeSchedulePayload } from './createDowntimeSchedule';
export interface DowntimeScheduleUpdatePayload {
data: DowntimeSchedulePayload;
id?: number;
}
export interface PayloadProps {
status: string;
data: string;
}
const updateDowntimeSchedule = async (
props: DowntimeScheduleUpdatePayload,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/downtime_schedules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default updateDowntimeSchedule;

View File

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

View File

@@ -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(),
});
};

View File

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

View File

@@ -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()}`);
}
}

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { VIEW_TYPES, VIEWS } from './constants';
@@ -330,10 +331,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -352,10 +350,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -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(),
})),
});
}

View File

@@ -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(),
});
};

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { RuletypesAlertTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { DataSource } from 'types/common/queryBuilder';
@@ -9,17 +8,3 @@ export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,
[AlertTypes.EXCEPTIONS_BASED_ALERT]: DataSource.TRACES,
};
export function dataSourceForAlertType(
alertType: RuletypesAlertTypeDTO | undefined,
): DataSource {
switch (alertType) {
case RuletypesAlertTypeDTO.LOGS_BASED_ALERT:
return DataSource.LOGS;
case RuletypesAlertTypeDTO.TRACES_BASED_ALERT:
case RuletypesAlertTypeDTO.EXCEPTIONS_BASED_ALERT:
return DataSource.TRACES;
default:
return DataSource.METRICS;
}
}

View File

@@ -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',
})}

View File

@@ -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]);

View File

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

View File

@@ -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(
() => (

View File

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

View File

@@ -6,7 +6,9 @@ import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/ut
import * as useSafeNavigateHook from 'hooks/useSafeNavigate';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import * as rulesHook from '../../../../api/generated/services/rules';
import * as useCreateAlertRuleHook from '../../../../hooks/alerts/useCreateAlertRule';
import * as useTestAlertRuleHook from '../../../../hooks/alerts/useTestAlertRule';
import * as useUpdateAlertRuleHook from '../../../../hooks/alerts/useUpdateAlertRule';
import { CreateAlertProvider } from '../../context';
import CreateAlertHeader from '../CreateAlertHeader';
@@ -15,15 +17,15 @@ jest.spyOn(useSafeNavigateHook, 'useSafeNavigate').mockReturnValue({
safeNavigate: mockSafeNavigate,
});
jest.spyOn(rulesHook, 'useCreateRule').mockReturnValue({
jest.spyOn(useCreateAlertRuleHook, 'useCreateAlertRule').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
} as any);
jest.spyOn(rulesHook, 'useTestRule').mockReturnValue({
jest.spyOn(useTestAlertRuleHook, 'useTestAlertRule').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
} as any);
jest.spyOn(rulesHook, 'useUpdateRuleByID').mockReturnValue({
jest.spyOn(useUpdateAlertRuleHook, 'useUpdateAlertRule').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
} as any);

View File

@@ -34,7 +34,6 @@ export const createMockAlertContextState = (
isUpdatingAlertRule: false,
updateAlertRule: jest.fn(),
isEditMode: false,
ruleId: '',
...overrides,
});

View File

@@ -1,15 +1,9 @@
import { useCallback, useMemo } from 'react';
import { toast } from '@signozhq/ui';
import { Button, Tooltip, Typography } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Check, Loader, Send, X } from 'lucide-react';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { toPostableRuleDTO } from 'types/api/alerts/convert';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import { useCreateAlertState } from '../context';
@@ -36,20 +30,9 @@ function Footer(): JSX.Element {
updateAlertRule,
isUpdatingAlertRule,
isEditMode,
ruleId,
} = useCreateAlertState();
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const { showErrorModal } = useErrorModal();
const handleApiError = useCallback(
(error: unknown): void => {
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
);
},
[showErrorModal],
);
const handleDiscard = (e: React.MouseEvent): void => {
discardAlertRule();
@@ -88,21 +71,20 @@ function Footer(): JSX.Element {
notificationSettings,
query: currentQuery,
});
testAlertRule(
{ data: toPostableRuleDTO(payload) },
{
onSuccess: (response) => {
if (response.data?.alertCount === 0) {
toast.error(
'No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.',
);
return;
}
toast.success('Test notification sent successfully');
},
onError: handleApiError,
testAlertRule(payload, {
onSuccess: (response) => {
if (response.payload?.data?.alertCount === 0) {
toast.error(
'No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.',
);
return;
}
toast.success('Test notification sent successfully');
},
);
onError: (error) => {
toast.error(error.message);
},
});
}, [
alertType,
basicAlertState,
@@ -125,30 +107,25 @@ function Footer(): JSX.Element {
query: currentQuery,
});
if (isEditMode) {
updateAlertRule(
{
pathParams: { id: ruleId },
data: toPostableRuleDTO(payload),
updateAlertRule(payload, {
onSuccess: () => {
toast.success('Alert rule updated successfully');
safeNavigate('/alerts');
},
{
onSuccess: () => {
toast.success('Alert rule updated successfully');
safeNavigate('/alerts');
},
onError: handleApiError,
onError: (error) => {
toast.error(error.message);
},
);
});
} else {
createAlertRule(
{ data: toPostableRuleDTO(payload) },
{
onSuccess: () => {
toast.success('Alert rule created successfully');
safeNavigate('/alerts');
},
onError: handleApiError,
createAlertRule(payload, {
onSuccess: () => {
toast.success('Alert rule created successfully');
safeNavigate('/alerts');
},
);
onError: (error) => {
toast.error(error.message);
},
});
}
}, [
alertType,
@@ -159,11 +136,9 @@ function Footer(): JSX.Element {
notificationSettings,
currentQuery,
isEditMode,
ruleId,
updateAlertRule,
createAlertRule,
safeNavigate,
handleApiError,
]);
const disableButtons =

View File

@@ -12,11 +12,6 @@ import Footer from '../Footer';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
jest.mock('providers/ErrorModalProvider', () => ({
useErrorModal: (): { showErrorModal: jest.Mock } => ({
showErrorModal: jest.fn(),
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: jest.fn(),

View File

@@ -10,13 +10,11 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import {
useCreateRule,
useTestRule,
useUpdateRuleByID,
} from 'api/generated/services/rules';
import { QueryParams } from 'constants/query';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { useCreateAlertRule } from 'hooks/alerts/useCreateAlertRule';
import { useTestAlertRule } from 'hooks/alerts/useTestAlertRule';
import { useUpdateAlertRule } from 'hooks/alerts/useUpdateAlertRule';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -217,14 +215,17 @@ export function CreateAlertProvider(
const {
mutate: createAlertRule,
isLoading: isCreatingAlertRule,
} = useCreateRule();
} = useCreateAlertRule();
const { mutate: testAlertRule, isLoading: isTestingAlertRule } = useTestRule();
const {
mutate: testAlertRule,
isLoading: isTestingAlertRule,
} = useTestAlertRule();
const {
mutate: updateAlertRule,
isLoading: isUpdatingAlertRule,
} = useUpdateRuleByID();
} = useUpdateAlertRule(ruleId || '');
const contextValue: ICreateAlertContextProps = useMemo(
() => ({
@@ -248,7 +249,6 @@ export function CreateAlertProvider(
updateAlertRule,
isUpdatingAlertRule,
isEditMode: isEditMode || false,
ruleId: ruleId || '',
}),
[
createAlertState,
@@ -267,7 +267,6 @@ export function CreateAlertProvider(
updateAlertRule,
isUpdatingAlertRule,
isEditMode,
ruleId,
],
);

View File

@@ -1,15 +1,12 @@
import { Dispatch } from 'react';
import { UseMutateFunction } from 'react-query';
import type {
CreateRule201,
RenderErrorResponseDTO,
RuletypesPostableRuleDTO,
TestRule200,
UpdateRuleByIDPathParameters,
} from 'api/generated/services/sigNoz.schemas';
import type { BodyType, ErrorType } from 'api/generatedAPIInstance';
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
import { UpdateAlertRuleResponse } from 'api/alerts/updateAlertRule';
import { Dayjs } from 'dayjs';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
import { Labels } from 'types/api/alerts/def';
export interface ICreateAlertContextProps {
@@ -27,33 +24,27 @@ export interface ICreateAlertContextProps {
setNotificationSettings: Dispatch<NotificationSettingsAction>;
isCreatingAlertRule: boolean;
createAlertRule: UseMutateFunction<
CreateRule201,
ErrorType<unknown>,
{ data: BodyType<RuletypesPostableRuleDTO> },
SuccessResponse<CreateAlertRuleResponse, unknown> | ErrorResponse,
Error,
PostableAlertRuleV2,
unknown
>;
isTestingAlertRule: boolean;
testAlertRule: UseMutateFunction<
TestRule200,
ErrorType<unknown>,
{ data: BodyType<RuletypesPostableRuleDTO> },
SuccessResponse<TestAlertRuleResponse, unknown> | ErrorResponse,
Error,
PostableAlertRuleV2,
unknown
>;
discardAlertRule: () => void;
isUpdatingAlertRule: boolean;
updateAlertRule: UseMutateFunction<
Awaited<
ReturnType<typeof import('api/generated/services/rules').updateRuleByID>
>,
ErrorType<RenderErrorResponseDTO>,
{
pathParams: UpdateRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
SuccessResponse<UpdateAlertRuleResponse, unknown> | ErrorResponse,
Error,
PostableAlertRuleV2,
unknown
>;
isEditMode: boolean;
ruleId: string;
}
export interface ICreateAlertProviderProps {

View File

@@ -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');
}}
>

View File

@@ -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);
}
}}
/>

View File

@@ -1,14 +1,15 @@
import { RuletypesAlertTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { dataSourceForAlertType } from 'constants/alerts';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export function sanitizeDefaultAlertQuery(
query: Query,
alertType: RuletypesAlertTypeDTO | undefined,
alertType: AlertTypes,
): Query {
// If there are no queries, add a default one based on the alert type
if (query.builder.queryData.length === 0) {
const dataSource = dataSourceForAlertType(alertType);
const dataSource = ALERTS_DATA_SOURCE_MAP[alertType];
query.builder.queryData.push(initialQueryBuilderFormValuesMap[dataSource]);
}
return query;

View File

@@ -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]);

View File

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

View File

@@ -6,15 +6,9 @@ import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
createRule,
testRule,
updateRuleByID,
} from 'api/generated/services/rules';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
@@ -38,16 +32,13 @@ import { isEmpty, isEqual } from 'lodash-es';
import { BellDot, ExternalLink } from 'lucide-react';
import Tabs2 from 'periscope/components/Tabs2';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { toPostableRuleDTOFromAlertDef } from 'types/api/alerts/convert';
import {
AlertDef,
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import APIError from 'types/api/error';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
@@ -55,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';
@@ -377,7 +369,6 @@ function FormAlertRules({
redirectWithQueryBuilderData(query);
};
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const validatePromParams = useCallback((): boolean => {
let retval = true;
@@ -543,47 +534,59 @@ function FormAlertRules({
};
try {
if (ruleId && !isEmpty(ruleId)) {
await updateRuleByID(
{ id: ruleId },
toPostableRuleDTOFromAlertDef(postableAlert),
);
const apiReq =
ruleId && !isEmpty(ruleId)
? { data: postableAlert, id: ruleId }
: { data: postableAlert };
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
logData = {
status: 'success',
statusMessage: isNewRule ? t('rule_created') : t('rule_edited'),
};
notifications.success({
message: 'Success',
description: logData.statusMessage,
});
// invalidate rule in cache
ruleCache.invalidateQueries([
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
`${ruleId}`,
]);
// eslint-disable-next-line sonarjs/no-identical-functions
setTimeout(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} else {
await createRule(toPostableRuleDTOFromAlertDef(postableAlert));
logData = {
status: 'error',
statusMessage: response.error || t('unexpected_error'),
};
notifications.error({
message: 'Error',
description: logData.statusMessage,
});
}
logData = {
status: 'success',
statusMessage: isNewRule ? t('rule_created') : t('rule_edited'),
};
notifications.success({
message: 'Success',
description: logData.statusMessage,
});
// invalidate rule in cache
ruleCache.invalidateQueries([
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
`${ruleId}`,
]);
// eslint-disable-next-line sonarjs/no-identical-functions
setTimeout(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} catch (e) {
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
logData = {
status: 'error',
statusMessage: apiError?.getErrorMessage() || t('unexpected_error'),
statusMessage: t('unexpected_error'),
};
showErrorModal(apiError as APIError);
notifications.error({
message: 'Error',
description: logData.statusMessage,
});
}
setLoading(false);
@@ -639,30 +642,39 @@ function FormAlertRules({
let statusResponse = { status: 'failed', message: '' };
setLoading(true);
try {
const response = await testRule(
toPostableRuleDTOFromAlertDef(postableAlert),
);
const response = await testAlertApi({ data: postableAlert });
if (response.data?.alertCount === 0) {
if (response.statusCode === 200) {
const { payload } = response;
if (payload?.alertCount === 0) {
notifications.error({
message: 'Error',
description: t('no_alerts_found'),
});
statusResponse = { status: 'failed', message: t('no_alerts_found') };
} else {
notifications.success({
message: 'Success',
description: t('rule_test_fired'),
});
statusResponse = { status: 'success', message: t('rule_test_fired') };
}
} else {
notifications.error({
message: 'Error',
description: t('no_alerts_found'),
description: response.error || t('unexpected_error'),
});
statusResponse = { status: 'failed', message: t('no_alerts_found') };
} else {
notifications.success({
message: 'Success',
description: t('rule_test_fired'),
});
statusResponse = { status: 'success', message: t('rule_test_fired') };
statusResponse = {
status: 'failed',
message: response.error || t('unexpected_error'),
};
}
} catch (e) {
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
statusResponse = {
status: 'failed',
message: apiError?.getErrorMessage() || t('unexpected_error'),
};
showErrorModal(apiError as APIError);
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
statusResponse = { status: 'failed', message: t('unexpected_error') };
}
setLoading(false);
logEvent('Alert: Test notification', {
@@ -760,7 +772,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
openInNewTab(url);
}
}

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { Link, useLocation } from 'react-router-dom';
import { Button, Skeleton, Tag } from 'antd';
import getAll from 'api/alerts/getAll';
import logEvent from 'api/common/logEvent';
import { useListRules } from 'api/generated/services/rules';
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
@@ -11,7 +11,7 @@ import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/map
import { ArrowRight, ArrowUpRight, Plus } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { toCompositeMetricQuery } from 'types/api/alerts/convert';
import { GettableAlert } from 'types/api/alerts/get';
import { USER_ROLES } from 'types/roles';
import beaconUrl from '@/assets/Icons/beacon.svg';
@@ -28,23 +28,22 @@ export default function AlertRules({
const { user } = useAppContext();
const [rulesExist, setRulesExist] = useState(false);
const [sortedAlertRules, setSortedAlertRules] = useState<RuletypesRuleDTO[]>(
[],
);
const [sortedAlertRules, setSortedAlertRules] = useState<GettableAlert[]>([]);
const location = useLocation();
const params = new URLSearchParams(location.search);
// Fetch Alerts
const { data: alerts, isError, isLoading } = useListRules({
query: { cacheTime: 0 },
const { data: alerts, isError, isLoading } = useQuery('allAlerts', {
queryFn: getAll,
cacheTime: 0,
});
useEffect(() => {
const rules = alerts?.data ?? [];
const rules = alerts?.payload || [];
setRulesExist(rules.length > 0);
const sortedRules = [...rules].sort((a, b) => {
const sortedRules = rules.sort((a, b) => {
// First, prioritize firing alerts
if (a.state === 'firing' && b.state !== 'firing') {
return -1;
@@ -53,10 +52,10 @@ export default function AlertRules({
return 1;
}
// Then sort by updatedAt timestamp
return (
new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime()
);
// Then sort by updateAt timestamp
const aUpdateAt = new Date(a.updateAt).getTime();
const bUpdateAt = new Date(b.updateAt).getTime();
return bUpdateAt - aUpdateAt;
});
if (sortedRules.length > 0 && !loadingUserPreferences) {
@@ -119,27 +118,22 @@ export default function AlertRules({
</div>
);
const onEditHandler = (record: RuletypesRuleDTO) => (): void => {
const onEditHandler = (record: GettableAlert) => (): void => {
logEvent('Homepage: Alert clicked', {
ruleId: record.id,
ruleName: record.alert,
ruleState: record.state,
});
const compositeQuery = mapQueryDataFromApi(
toCompositeMetricQuery(record.condition.compositeQuery),
);
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
const panelType = record.condition.compositeQuery.panelType;
if (panelType) {
params.set(QueryParams.panelTypes, panelType);
}
params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType);
params.set(QueryParams.ruleId, record.id);
params.set(QueryParams.ruleId, record.id.toString());
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
};
@@ -175,9 +169,9 @@ export default function AlertRules({
<div className="alert-rule-item-description home-data-item-tag">
<Tag color={rule?.labels?.severity}>{rule?.labels?.severity}</Tag>
{rule.state === 'firing' && (
{rule?.state === 'firing' && (
<Tag color="red" className="firing-tag">
{rule.state}
{rule?.state}
</Tag>
)}
</div>

View File

@@ -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());
}

View File

@@ -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);
}
};

View File

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

View File

@@ -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);
}
}}
>

View File

@@ -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);
}
}}
>

View File

@@ -49,6 +49,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -414,10 +415,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -436,10 +434,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -429,10 +430,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -451,10 +449,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -433,10 +434,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -455,10 +453,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import JobEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -427,10 +428,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -449,10 +447,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -419,10 +420,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -441,10 +439,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NodeLogs from '../../EntityDetailsUtils/EntityLogs';
@@ -416,10 +417,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -438,10 +436,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import PodEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -435,10 +436,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -457,10 +455,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -53,6 +53,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -431,10 +432,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -453,10 +451,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

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

View File

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

View File

@@ -1,16 +1,9 @@
import { Dispatch, SetStateAction, useState } from 'react';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { deleteRuleByID } from 'api/generated/services/rules';
import type {
RenderErrorResponseDTO,
RuletypesRuleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import deleteAlerts from 'api/alerts/delete';
import { State } from 'hooks/useFetch';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
import APIError from 'types/api/error';
import { GettableAlert } from 'types/api/alerts/get';
import { ColumnButton } from './styles';
@@ -29,31 +22,48 @@ function DeleteAlert({
payload: undefined,
});
const { showErrorModal } = useErrorModal();
const defaultErrorMessage = 'Something went wrong';
const onDeleteHandler = async (id: string): Promise<void> => {
try {
await deleteRuleByID({ id });
setData((state) => state.filter((alert) => alert.id !== id));
setDeleteAlertState((state) => ({
...state,
loading: false,
}));
notifications.success({
message: 'Success',
const response = await deleteAlerts({
id,
});
if (response.statusCode === 200) {
setData((state) => state.filter((alert) => alert.id !== id));
setDeleteAlertState((state) => ({
...state,
loading: false,
payload: response.payload,
}));
notifications.success({
message: 'Success',
});
} else {
setDeleteAlertState((state) => ({
...state,
loading: false,
error: true,
errorMessage: response.error || defaultErrorMessage,
}));
notifications.error({
message: response.error || defaultErrorMessage,
});
}
} catch (error) {
setDeleteAlertState((state) => ({
...state,
loading: false,
error: true,
errorMessage: defaultErrorMessage,
}));
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
);
notifications.error({
message: defaultErrorMessage,
});
}
};
@@ -78,8 +88,8 @@ function DeleteAlert({
}
interface DeleteAlertProps {
id: string;
setData: Dispatch<SetStateAction<RuletypesRuleDTO[]>>;
id: GettableAlert['id'];
setData: Dispatch<SetStateAction<GettableAlert[]>>;
notifications: NotificationInstance;
}

View File

@@ -4,16 +4,8 @@ import { UseQueryResult } from 'react-query';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Flex, Input, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { createRule } from 'api/generated/services/rules';
import type {
ListRules200,
RenderErrorResponseDTO,
RuletypesRuleDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { AxiosError } from 'axios';
import DropDown from 'components/DropDown/DropDown';
import {
DynamicColumnsKey,
@@ -35,9 +27,9 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { toCompositeMetricQuery } from 'types/api/alerts/convert';
import APIError from 'types/api/error';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
import { isModifierKeyPressed } from 'utils/app';
import DeleteAlert from './DeleteAlert';
@@ -66,7 +58,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const paginationParam = params.get('page');
const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || '');
const [data, setData] = useState<RuletypesRuleDTO[]>(() => {
const [data, setData] = useState<GettableAlert[]>(() => {
const value = searchString.toLowerCase();
const filteredData = filterAlerts(allAlertRules, value);
return filteredData || [];
@@ -78,7 +70,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
? orderQueryParam
: null;
const { sortedInfo, handleChange } = useSortableTable<RuletypesRuleDTO>(
const { sortedInfo, handleChange } = useSortableTable<GettableAlert>(
sortingOrder,
orderColumnParam || '',
searchString,
@@ -91,7 +83,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const { data: refetchData, status } = await refetch();
if (status === 'success') {
const value = searchString.toLowerCase();
const filteredData = filterAlerts(refetchData?.data ?? [], value);
const filteredData = filterAlerts(refetchData.payload || [], value);
setData(filteredData || []);
}
if (status === 'error') {
@@ -102,7 +94,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
})();
}, 30000);
const { showErrorModal } = useErrorModal();
const handleError = useCallback((): void => {
notificationsApi.error({
message: t('something_went_wrong'),
});
}, [notificationsApi, t]);
const onClickNewAlertHandler = useCallback(
(e: React.MouseEvent): void => {
@@ -119,24 +115,21 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
);
const onEditHandler = (
record: RuletypesRuleDTO,
record: GettableAlert,
options?: { newTab?: boolean },
): void => {
const compositeQuery = sanitizeDefaultAlertQuery(
mapQueryDataFromApi(toCompositeMetricQuery(record.condition.compositeQuery)),
record.alertType,
mapQueryDataFromApi(record.condition.compositeQuery),
record.alertType as AlertTypes,
);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
const panelType = record.condition.compositeQuery.panelType;
if (panelType) {
params.set(QueryParams.panelTypes, panelType);
}
params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType);
params.set(QueryParams.ruleId, record.id);
params.set(QueryParams.ruleId, record.id.toString());
setEditLoader(false);
@@ -146,41 +139,47 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
};
const onCloneHandler = (
originalAlert: RuletypesRuleDTO,
originalAlert: GettableAlert,
) => async (): Promise<void> => {
const copyAlert: RuletypesRuleDTO = {
const copyAlert = {
...originalAlert,
alert: `${originalAlert.alert} - Copy`,
alert: originalAlert.alert.concat(' - Copy'),
};
const apiReq = { data: copyAlert };
try {
setCloneLoader(true);
await createRule(copyAlert);
const response = await saveAlertApi(apiReq);
notificationsApi.success({
message: 'Success',
description: 'Alert cloned successfully',
});
if (response.statusCode === 200) {
notificationsApi.success({
message: 'Success',
description: 'Alert cloned successfully',
});
const { data: refetchData, status } = await refetch();
const rules = refetchData?.data;
if (status === 'success' && rules) {
setData(rules);
setTimeout(() => {
const clonedAlert = rules[rules.length - 1];
params.set(QueryParams.ruleId, String(clonedAlert.id));
safeNavigate(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
}, 2000);
}
if (status === 'error') {
const { data: refetchData, status } = await refetch();
if (status === 'success' && refetchData.payload) {
setData(refetchData.payload || []);
setTimeout(() => {
const clonedAlert = refetchData.payload[refetchData.payload.length - 1];
params.set(QueryParams.ruleId, String(clonedAlert.id));
safeNavigate(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
}, 2000);
}
if (status === 'error') {
notificationsApi.error({
message: t('something_went_wrong'),
});
}
} else {
notificationsApi.error({
message: t('something_went_wrong'),
message: 'Error',
description: response.error || t('something_went_wrong'),
});
}
} catch (error) {
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
);
handleError();
console.error(error);
} finally {
setCloneLoader(false);
}
@@ -193,16 +192,16 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setData(filteredData);
});
const dynamicColumns: ColumnsType<RuletypesRuleDTO> = [
const dynamicColumns: ColumnsType<GettableAlert> = [
{
title: 'Created At',
dataIndex: 'createdAt',
dataIndex: 'createAt',
width: 80,
key: DynamicColumnsKey.CreatedAt,
align: 'center',
sorter: (a: RuletypesRuleDTO, b: RuletypesRuleDTO): number => {
const prev = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const next = b.createdAt ? new Date(b.createdAt).getTime() : 0;
sorter: (a: GettableAlert, b: GettableAlert): number => {
const prev = new Date(a.createAt).getTime();
const next = new Date(b.createAt).getTime();
return prev - next;
},
@@ -214,20 +213,20 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
},
{
title: 'Created By',
dataIndex: 'createdBy',
dataIndex: 'createBy',
width: 80,
key: DynamicColumnsKey.CreatedBy,
align: 'center',
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
dataIndex: 'updateAt',
width: 80,
key: DynamicColumnsKey.UpdatedAt,
align: 'center',
sorter: (a: RuletypesRuleDTO, b: RuletypesRuleDTO): number => {
const prev = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
const next = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
sorter: (a: GettableAlert, b: GettableAlert): number => {
const prev = new Date(a.updateAt).getTime();
const next = new Date(b.updateAt).getTime();
return prev - next;
},
@@ -239,14 +238,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
},
{
title: 'Updated By',
dataIndex: 'updatedBy',
dataIndex: 'updateBy',
width: 80,
key: DynamicColumnsKey.UpdatedBy,
align: 'center',
},
];
const columns: ColumnsType<RuletypesRuleDTO> = [
const columns: ColumnsType<GettableAlert> = [
{
title: 'Status',
dataIndex: 'state',
@@ -323,7 +322,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dataIndex: 'id',
key: 'action',
width: 10,
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => (
render: (id: GettableAlert['id'], record): JSX.Element => (
<div data-testid="alert-actions">
<DropDown
onDropDownItemClick={(item): void =>
@@ -332,9 +331,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
element={[
<ToggleAlertState
key="1"
disabled={record.disabled ?? false}
disabled={record.disabled}
setData={setData}
id={id ?? ''}
id={id}
/>,
<ColumnButton
key="2"
@@ -366,7 +365,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
key="4"
notifications={notificationsApi}
setData={setData}
id={id ?? ''}
id={id}
/>,
]}
/>
@@ -421,10 +420,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}
interface ListAlertProps {
allAlertRules: RuletypesRuleDTO[];
allAlertRules: GettableAlert[];
refetch: UseQueryResult<
ListRules200,
ErrorType<RenderErrorResponseDTO>
ErrorResponse | SuccessResponse<GettableAlert[]>
>['refetch'];
}

View File

@@ -1,5 +1,5 @@
import { Tag } from 'antd';
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
import { GettableAlert } from 'types/api/alerts/get';
function Status({ status }: StatusProps): JSX.Element {
switch (status) {
@@ -26,7 +26,7 @@ function Status({ status }: StatusProps): JSX.Element {
}
interface StatusProps {
status: RuletypesRuleDTO['state'];
status: GettableAlert['state'];
}
export default Status;

View File

@@ -1,15 +1,9 @@
import { Dispatch, SetStateAction, useState } from 'react';
import { patchRulePartial } from 'api/alerts/patchRulePartial';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type {
RenderErrorResponseDTO,
RuletypesRuleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import patchAlert from 'api/alerts/patch';
import { State } from 'hooks/useFetch';
import { useNotifications } from 'hooks/useNotifications';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { GettableAlert } from 'types/api/alerts/get';
import { PayloadProps as PatchPayloadProps } from 'types/api/alerts/patch';
import { ColumnButton } from './styles';
@@ -18,7 +12,7 @@ function ToggleAlertState({
disabled,
setData,
}: ToggleAlertStateProps): JSX.Element {
const [apiStatus, setAPIStatus] = useState<State<RuletypesRuleDTO>>({
const [apiStatus, setAPIStatus] = useState<State<PatchPayloadProps>>({
error: false,
errorMessage: '',
loading: false,
@@ -27,7 +21,8 @@ function ToggleAlertState({
});
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const defaultErrorMessage = 'Something went wrong';
const onToggleHandler = async (
id: string,
@@ -39,40 +34,58 @@ function ToggleAlertState({
loading: true,
}));
const response = await patchRulePartial(id, { disabled });
const { data: updatedRule } = response;
setData((state) =>
state.map((alert) => {
if (alert.id === id) {
return {
...alert,
disabled: updatedRule.disabled,
state: updatedRule.state,
};
}
return alert;
}),
);
setAPIStatus((state) => ({
...state,
loading: false,
payload: updatedRule,
}));
notifications.success({
message: 'Success',
const response = await patchAlert({
id,
data: {
disabled,
},
});
if (response.statusCode === 200) {
setData((state) =>
state.map((alert) => {
if (alert.id === id) {
return {
...alert,
disabled: response.payload.disabled,
state: response.payload.state,
};
}
return alert;
}),
);
setAPIStatus((state) => ({
...state,
loading: false,
payload: response.payload,
}));
notifications.success({
message: 'Success',
});
} else {
setAPIStatus((state) => ({
...state,
loading: false,
error: true,
errorMessage: response.error || defaultErrorMessage,
}));
notifications.error({
message: response.error || defaultErrorMessage,
});
}
} catch (error) {
setAPIStatus((state) => ({
...state,
loading: false,
error: true,
errorMessage: defaultErrorMessage,
}));
showErrorModal(
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
);
notifications.error({
message: defaultErrorMessage,
});
}
};
@@ -89,9 +102,9 @@ function ToggleAlertState({
}
interface ToggleAlertStateProps {
id: string;
id: GettableAlert['id'];
disabled: boolean;
setData: Dispatch<SetStateAction<RuletypesRuleDTO[]>>;
setData: Dispatch<SetStateAction<GettableAlert[]>>;
}
export default ToggleAlertState;

View File

@@ -1,69 +1,52 @@
import type {
RuletypesAlertStateDTO,
RuletypesCompareOperatorDTO,
RuletypesMatchTypeDTO,
RuletypesPanelTypeDTO,
RuletypesQueryTypeDTO,
RuletypesRuleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { GettableAlert } from 'types/api/alerts/get';
import { filterAlerts } from '../utils';
describe('filterAlerts', () => {
const mockAlertBase: Partial<RuletypesRuleDTO> = {
state: 'active' as RuletypesAlertStateDTO,
const mockAlertBase: Partial<GettableAlert> = {
state: 'active',
disabled: false,
createdAt: new Date('2024-01-01T00:00:00Z'),
createdBy: 'test-user',
updatedAt: new Date('2024-01-01T00:00:00Z'),
updatedBy: 'test-user',
createAt: '2024-01-01T00:00:00Z',
createBy: 'test-user',
updateAt: '2024-01-01T00:00:00Z',
updateBy: 'test-user',
version: '1',
condition: {
compositeQuery: {
queries: [],
panelType: 'graph' as RuletypesPanelTypeDTO,
queryType: 'builder' as RuletypesQueryTypeDTO,
},
matchType: 'at_least_once' as RuletypesMatchTypeDTO,
op: 'above' as RuletypesCompareOperatorDTO,
},
ruleType: 'threshold_rule' as RuletypesRuleDTO['ruleType'],
};
const mockAlerts: RuletypesRuleDTO[] = [
const mockAlerts: GettableAlert[] = [
{
...mockAlertBase,
id: '1',
alert: 'High CPU Usage',
alertType: 'METRIC_BASED_ALERT',
alertType: 'metrics',
labels: {
severity: 'warning',
status: 'ok',
environment: 'production',
},
} as RuletypesRuleDTO,
} as GettableAlert,
{
...mockAlertBase,
id: '2',
alert: 'Memory Leak Detected',
alertType: 'METRIC_BASED_ALERT',
alertType: 'metrics',
labels: {
severity: 'critical',
status: 'firing',
environment: 'staging',
},
} as RuletypesRuleDTO,
} as GettableAlert,
{
...mockAlertBase,
id: '3',
alert: 'Database Connection Error',
alertType: 'METRIC_BASED_ALERT',
alertType: 'metrics',
labels: {
severity: 'error',
status: 'pending',
environment: 'production',
},
} as RuletypesRuleDTO,
} as GettableAlert,
];
it('should return all alerts when filter is empty', () => {
@@ -114,14 +97,14 @@ describe('filterAlerts', () => {
});
it('should handle alerts with missing labels', () => {
const alertsWithMissingLabels: RuletypesRuleDTO[] = [
const alertsWithMissingLabels: GettableAlert[] = [
{
...mockAlertBase,
id: '4',
alert: 'Test Alert',
alertType: 'METRIC_BASED_ALERT',
alertType: 'metrics',
labels: undefined,
} as RuletypesRuleDTO,
} as GettableAlert,
];
const result = filterAlerts(alertsWithMissingLabels, 'test');
expect(result).toHaveLength(1);
@@ -129,16 +112,16 @@ describe('filterAlerts', () => {
});
it('should handle alerts with missing alert name', () => {
const alertsWithMissingName: RuletypesRuleDTO[] = [
const alertsWithMissingName: GettableAlert[] = [
{
...mockAlertBase,
id: '5',
alert: '',
alertType: 'METRIC_BASED_ALERT',
alertType: 'metrics',
labels: {
severity: 'warning',
},
} as RuletypesRuleDTO,
} as GettableAlert,
];
const result = filterAlerts(alertsWithMissingName, 'warning');
expect(result).toHaveLength(1);

View File

@@ -1,66 +1,78 @@
import { useEffect, useMemo, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { Space } from 'antd';
import getAll from 'api/alerts/getAll';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRules } from 'api/generated/services/rules';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import Spinner from 'components/Spinner';
import { useNotifications } from 'hooks/useNotifications';
import { isUndefined } from 'lodash-es';
import { AlertsEmptyState } from './AlertsEmptyState/AlertsEmptyState';
import ListAlert from './ListAlert';
function ListAlertRules(): JSX.Element {
const { t } = useTranslation('common');
const { data, isError, isLoading, refetch, error } = useListRules({
query: { cacheTime: 0 },
const { data, isError, isLoading, refetch, status } = useQuery('allAlerts', {
queryFn: getAll,
cacheTime: 0,
});
const rules = data?.data ?? [];
const hasLoaded = !isLoading && data !== undefined;
const logEventCalledRef = useRef(false);
const { notifications } = useNotifications();
const apiError = useMemo(
() => convertToApiError(error as AxiosError<RenderErrorResponseDTO> | null),
[error],
);
useEffect(() => {
if (!logEventCalledRef.current && hasLoaded) {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
logEvent('Alert: List page visited', {
number: rules.length,
number: data?.payload?.length,
});
logEventCalledRef.current = true;
}
}, [hasLoaded, rules.length]);
}, [data?.payload]);
useEffect(() => {
if (isError) {
if (status === 'error' || (status === 'success' && data.statusCode >= 400)) {
notifications.error({
message: apiError?.getErrorMessage() || t('something_went_wrong'),
message: data?.error || t('something_went_wrong'),
});
}
}, [isError, apiError, t, notifications]);
}, [data?.error, data?.statusCode, status, t, notifications]);
// api failed to load the data
if (isError) {
return <div>{apiError?.getErrorMessage() || t('something_went_wrong')}</div>;
return <div>{data?.error || t('something_went_wrong')}</div>;
}
if (isLoading || !data) {
return <Spinner height="75vh" tip="Loading Rules..." />;
// api is successful but error is present
if (status === 'success' && data.statusCode >= 400) {
return (
<ListAlert
{...{
allAlertRules: [],
refetch,
}}
/>
);
}
if (rules.length === 0) {
if (status === 'success' && !data.payload?.length) {
return <AlertsEmptyState />;
}
// in case of loading
if (isLoading || !data?.payload) {
return <Spinner height="75vh" tip="Loading Rules..." />;
}
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<ListAlert allAlertRules={rules} refetch={refetch} />
<ListAlert
{...{
allAlertRules: data.payload,
refetch,
}}
/>
</Space>
);
}

View File

@@ -1,18 +1,19 @@
import logEvent from 'api/common/logEvent';
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
import { dataSourceForAlertType } from 'constants/alerts';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
export const filterAlerts = (
allAlertRules: RuletypesRuleDTO[],
allAlertRules: GettableAlert[],
filter: string,
): RuletypesRuleDTO[] => {
): GettableAlert[] => {
if (!filter.trim()) {
return allAlertRules;
}
const value = filter.trim().toLowerCase();
return allAlertRules.filter((alert) => {
const alertName = alert.alert.toLowerCase();
const alertName = alert.alert?.toLowerCase();
const severity = alert.labels?.severity?.toLowerCase();
// Create a string of all label keys and values for searching
@@ -22,7 +23,7 @@ export const filterAlerts = (
.toLowerCase();
return (
alertName.includes(value) ||
alertName?.includes(value) ||
severity?.includes(value) ||
labelSearchString.includes(value)
);
@@ -31,7 +32,7 @@ export const filterAlerts = (
export const alertActionLogEvent = (
action: string,
record: RuletypesRuleDTO,
record: GettableAlert,
): void => {
let actionValue = '';
switch (action) {
@@ -51,9 +52,9 @@ export const alertActionLogEvent = (
break;
}
logEvent('Alert: Action', {
ruleId: record.id,
dataSource: dataSourceForAlertType(record.alertType),
name: record.alert,
ruleId: record?.id,
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
name: record?.alert,
action: actionValue,
});
};

View File

@@ -83,6 +83,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';
@@ -457,7 +459,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
window.open(getLink(), '_blank');
openInNewTab(getLink());
}}
>
Open in New Tab
@@ -469,7 +471,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(`${window.location.origin}${getLink()}`);
setCopy(getAbsoluteUrl(getLink()));
}}
>
Copy Link

View File

@@ -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());
}

View File

@@ -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],
);

View File

@@ -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);
}

View File

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

View File

@@ -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' });

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

@@ -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: '',
};

View File

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

View File

@@ -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: '',
};

View File

@@ -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} />;
},
},

View File

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

View File

@@ -1,21 +1,21 @@
import React, { ChangeEvent, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useHistory } from 'react-router-dom';
import { PlusOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Form, Input, Tooltip, Typography } from 'antd';
import getAll from 'api/alerts/getAll';
import { useDeleteDowntimeSchedule } from 'api/plannedDowntime/deleteDowntimeSchedule';
import {
useDeleteDowntimeScheduleByID,
useListDowntimeSchedules,
} from 'api/generated/services/downtimeschedules';
import { useListRules } from 'api/generated/services/rules';
import type { RuletypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
DowntimeSchedules,
useGetAllDowntimeSchedules,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import dayjs from 'dayjs';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Search } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { USER_ROLES } from 'types/roles';
import 'dayjs/locale/en';
@@ -33,28 +33,28 @@ import './PlannedDowntime.styles.scss';
dayjs.locale('en');
export function PlannedDowntime(): JSX.Element {
const { data: alertsData, isError, isLoading } = useListRules({
query: { cacheTime: 0 },
const { data, isError, isLoading } = useQuery('allAlerts', {
queryFn: getAll,
cacheTime: 0,
});
const [isOpen, setIsOpen] = React.useState(false);
const [form] = Form.useForm();
const { user } = useAppContext();
const { showErrorModal } = useErrorModal();
const history = useHistory();
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] = useState<
Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>
Partial<DowntimeSchedules & { editMode: boolean }>
>(defautlInitialValues);
const downtimeSchedules = useListDowntimeSchedules();
const downtimeSchedules = useGetAllDowntimeSchedules();
const alertOptions = React.useMemo(
() =>
alertsData?.data?.map((i) => ({
data?.payload?.map((i) => ({
label: i.alert,
value: i.id,
})),
[alertsData],
[data],
);
useEffect(() => {
@@ -66,7 +66,7 @@ export function PlannedDowntime(): JSX.Element {
const [searchValue, setSearchValue] = React.useState<string | number>(
urlQuery.get('search') || '',
);
const [deleteData, setDeleteData] = useState<{ id: string; name: string }>();
const [deleteData, setDeleteData] = useState<{ id: number; name: string }>();
const [isEditMode, setEditMode] = useState<boolean>(false);
const updateUrlWithSearch = useDebouncedFn((value) => {
@@ -105,13 +105,12 @@ export function PlannedDowntime(): JSX.Element {
const {
mutateAsync: deleteDowntimeScheduleAsync,
isLoading: isDeleteLoading,
} = useDeleteDowntimeScheduleByID();
} = useDeleteDowntimeSchedule({ id: deleteData?.id });
const onDeleteHandler = (): void => {
deleteDowntimeHandler({
deleteDowntimeScheduleAsync,
notifications,
showErrorModal,
refetchAllSchedules,
deleteId: deleteData?.id,
hideDeleteDowntimeScheduleModal,

View File

@@ -14,18 +14,11 @@ import {
Typography,
} from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
createDowntimeSchedule,
updateDowntimeScheduleByID,
} from 'api/generated/services/downtimeschedules';
import type {
RuletypesPlannedMaintenanceDTO,
RuletypesPostablePlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
DowntimeSchedules,
Recurrence,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import { DowntimeScheduleUpdatePayload } from 'api/plannedDowntime/updateDowntimeSchedule';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import {
ModalButtonWrapper,
@@ -36,14 +29,15 @@ import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo, isEmpty } from 'lodash-es';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ALL_TIME_ZONES } from 'utils/timeZoneUtil';
import 'dayjs/locale/en';
import { SOMETHING_WENT_WRONG } from '../../constants/api';
import { showErrorNotification } from '../../utils/error';
import { AlertRuleTags } from './PlannedDowntimeList';
import {
createEditDowntimeSchedule,
getAlertOptionsFromIds,
getDurationInfo,
getEndTime,
@@ -68,9 +62,9 @@ interface PlannedDowntimeFormData {
name: string;
startTime: dayjs.Dayjs | string;
endTime: dayjs.Dayjs | string;
recurrence?: RuletypesRecurrenceDTO | null;
recurrence?: Recurrence | null;
alertRules: DefaultOptionType[];
recurrenceSelect?: RuletypesRecurrenceDTO;
recurrenceSelect?: Recurrence;
timezone?: string;
}
@@ -78,7 +72,7 @@ const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
interface PlannedDowntimeFormProps {
initialValues: Partial<
RuletypesPlannedMaintenanceDTO & {
DowntimeSchedules & {
editMode: boolean;
}
>;
@@ -117,9 +111,9 @@ export function PlannedDowntimeForm(
?.unit || 'm',
);
const [formData, setFormData] = useState<Partial<PlannedDowntimeFormData>>({
timezone: initialValues.schedule?.timezone,
});
const [formData, setFormData] = useState<PlannedDowntimeFormData>(
initialValues?.schedule as PlannedDowntimeFormData,
);
const [recurrenceType, setRecurrenceType] = useState<string | null>(
(initialValues.schedule?.recurrence?.repeatType as string) ||
@@ -131,7 +125,6 @@ export function PlannedDowntimeForm(
: undefined;
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const datePickerFooter = (mode: any): any =>
mode === 'time' ? (
@@ -141,54 +134,57 @@ export function PlannedDowntimeForm(
const saveHanlder = useCallback(
async (values: PlannedDowntimeFormData) => {
const shouldKeepLocalTime = !isEditMode;
const data: RuletypesPostablePlannedMaintenanceDTO = {
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
name: values.name,
schedule: {
startTime: new Date(
handleTimeConversion(
const createEditProps: DowntimeScheduleUpdatePayload = {
data: {
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
name: values.name,
schedule: {
startTime: handleTimeConversion(
values.startTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
),
timezone: values.timezone as string,
endTime: values.endTime
? new Date(
handleTimeConversion(
timezone: values.timezone,
endTime: values.endTime
? handleTimeConversion(
values.endTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
)
: undefined,
recurrence: values.recurrence as RuletypesRecurrenceDTO,
)
: undefined,
recurrence: values.recurrence as Recurrence,
},
},
id: isEditMode ? initialValues.id : undefined,
};
setSaveLoading(true);
try {
if (isEditMode && initialValues.id) {
await updateDowntimeScheduleByID({ id: initialValues.id }, data);
const response = await createEditDowntimeSchedule({ ...createEditProps });
if (response.message === 'success') {
setIsOpen(false);
notifications.success({
message: 'Success',
description: isEditMode
? 'Schedule updated successfully'
: 'Schedule created successfully',
});
refetchAllSchedules();
} else {
await createDowntimeSchedule(data);
notifications.error({
message: 'Error',
description:
typeof response.error === 'string'
? response.error
: response.error?.message || SOMETHING_WENT_WRONG,
});
}
setIsOpen(false);
notifications.success({
message: 'Success',
description: isEditMode
? 'Schedule updated successfully'
: 'Schedule created successfully',
});
refetchAllSchedules();
} catch (e: unknown) {
showErrorModal(
convertToApiError(e as AxiosError<RenderErrorResponseDTO>) as APIError,
);
showErrorNotification(notifications, e as Error);
}
setSaveLoading(false);
},
@@ -199,11 +195,10 @@ export function PlannedDowntimeForm(
refetchAllSchedules,
setIsOpen,
timezoneInitialValue,
showErrorModal,
],
);
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
const recurrenceData =
const recurrenceData: Recurrence | undefined =
values?.recurrence?.repeatType === recurrenceOptions.doesNotRepeat.value
? undefined
: {
@@ -230,10 +225,7 @@ export function PlannedDowntimeForm(
repeatType: values.recurrence?.repeatType,
};
const payloadValues = {
...values,
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
};
const payloadValues = { ...values, recurrence: recurrenceData };
await saveHanlder(payloadValues);
};
@@ -244,9 +236,11 @@ export function PlannedDowntimeForm(
];
const handleOk = async (): Promise<void> => {
await form.validateFields().catch(() => {
// antd renders inline field-level errors; nothing more to do here.
});
try {
await form.validateFields();
} catch (error) {
console.error(error);
}
};
const handleCancel = (): void => {
@@ -287,19 +281,18 @@ export function PlannedDowntimeForm(
: '',
recurrence: {
...initialValues.schedule?.recurrence,
repeatType: (!isScheduleRecurring(initialValues?.schedule)
repeatType: !isScheduleRecurring(initialValues?.schedule)
? recurrenceOptions.doesNotRepeat.value
: initialValues.schedule?.recurrence
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
duration: String(
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
?.value ?? '',
),
} as RuletypesRecurrenceDTO,
: (initialValues.schedule?.recurrence?.repeatType as string),
duration: getDurationInfo(
initialValues.schedule?.recurrence?.duration as string,
)?.value,
},
timezone: initialValues.schedule?.timezone as string,
};
return formData;
}, [initialValues, alertOptions]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValues]);
useEffect(() => {
setSelectedTags(formatedInitialValues.alertRules);
@@ -332,12 +325,7 @@ export function PlannedDowntimeForm(
const startTimeText = useMemo((): string => {
let startTime = formData?.startTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
startTime =
(formData?.recurrence?.startTime
? dayjs(formData.recurrence.startTime).toISOString()
: '') ||
formData?.startTime ||
'';
startTime = formData?.recurrence?.startTime || formData?.startTime || '';
}
if (!startTime) {
@@ -393,10 +381,7 @@ export function PlannedDowntimeForm(
const endTimeText = useMemo((): string => {
let endTime = formData?.endTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
endTime =
(formData?.recurrence?.endTime
? dayjs(formData.recurrence.endTime).toISOString()
: '') || '';
endTime = formData?.recurrence?.endTime || '';
if (!isEditMode && !endTime) {
endTime = formData?.endTime || '';

View File

@@ -12,15 +12,13 @@ import {
Typography,
} from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import type {
ListDowntimeSchedules200,
RenderErrorResponseDTO,
RuletypesPlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import {
DowntimeSchedules,
PayloadProps,
Recurrence,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import { AxiosError, AxiosResponse } from 'axios';
import cx from 'classnames';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CalendarClock, PenLine, Trash2 } from 'lucide-react';
@@ -145,7 +143,7 @@ export function CollapseListContent({
created_by_name?: string;
created_by_email?: string;
timeframe: [string | undefined | null, string | undefined | null];
repeats?: RuletypesRecurrenceDTO | null;
repeats?: Recurrence | null;
updated_at?: string;
updated_by_name?: string;
alertOptions?: DefaultOptionType[];
@@ -220,10 +218,10 @@ export function CollapseListContent({
export function CustomCollapseList(
props: DowntimeSchedulesTableData & {
setInitialValues: React.Dispatch<
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<DowntimeSchedules>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;
handleDeleteDowntime: (id: number, name: string) => void;
setEditMode: React.Dispatch<React.SetStateAction<boolean>>;
},
): JSX.Element {
@@ -243,19 +241,12 @@ export function CustomCollapseList(
kind,
} = props;
const scheduleTime = schedule?.startTime
? dayjs(schedule.startTime).toISOString()
: createdAt
? dayjs(createdAt).toISOString()
: '';
const scheduleTime = schedule?.startTime ? schedule.startTime : createdAt;
// Combine time and date
const formattedDateAndTime = `Start time ⎯ ${formatDateTime(
defaultTo(scheduleTime, ''),
)} ${schedule?.timezone}`;
const endTime = getEndTime({
kind,
schedule,
} as Partial<RuletypesPlannedMaintenanceDTO>);
const endTime = getEndTime({ kind, schedule });
return (
<>
@@ -266,10 +257,7 @@ export function CustomCollapseList(
duration={
schedule?.recurrence?.duration
? (schedule?.recurrence?.duration as string)
: getDuration(
schedule?.startTime ? dayjs(schedule.startTime).toISOString() : '',
schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '',
)
: getDuration(schedule?.startTime, schedule?.endTime)
}
name={defaultTo(name, '')}
handleEdit={(): void => {
@@ -278,23 +266,21 @@ export function CustomCollapseList(
setEditMode(true);
}}
handleDelete={(): void => {
handleDeleteDowntime(id ?? '', name || '');
handleDeleteDowntime(id, name || '');
}}
/>
}
key={id ?? ''}
key={id}
>
<CollapseListContent
created_at={createdAt ? dayjs(createdAt).toISOString() : ''}
created_at={defaultTo(createdAt, '')}
created_by_name={defaultTo(createdBy, '')}
timeframe={[
schedule?.startTime?.toString(),
typeof endTime === 'string' ? endTime : endTime?.toString(),
]}
repeats={
schedule?.recurrence as RuletypesRecurrenceDTO | null | undefined
}
updated_at={updatedAt ? dayjs(updatedAt).toISOString() : ''}
repeats={schedule?.recurrence}
updated_at={defaultTo(updatedAt, '')}
updated_by_name={defaultTo(updatedBy, '')}
alertOptions={alertOptions}
timezone={defaultTo(schedule?.timezone, '')}
@@ -309,7 +295,7 @@ export function CustomCollapseList(
);
}
export type DowntimeSchedulesTableData = RuletypesPlannedMaintenanceDTO & {
export type DowntimeSchedulesTableData = DowntimeSchedules & {
alertOptions: DefaultOptionType[];
};
@@ -323,15 +309,15 @@ export function PlannedDowntimeList({
searchValue,
}: {
downtimeSchedules: UseQueryResult<
ListDowntimeSchedules200,
ErrorType<RenderErrorResponseDTO>
AxiosResponse<PayloadProps, any>,
AxiosError<unknown, any>
>;
alertOptions: DefaultOptionType[];
setInitialValues: React.Dispatch<
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<DowntimeSchedules>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;
handleDeleteDowntime: (id: number, name: string) => void;
setEditMode: React.Dispatch<React.SetStateAction<boolean>>;
searchValue: string | number;
}): JSX.Element {
@@ -351,19 +337,19 @@ export function PlannedDowntimeList({
];
const { notifications } = useNotifications();
const tableData = [...(downtimeSchedules.data?.data || [])]
const tableData = (downtimeSchedules.data?.data?.data || [])
.sort((a, b): number => {
if (a?.updatedAt && b?.updatedAt) {
return dayjs(b.updatedAt).diff(dayjs(a.updatedAt));
return b.updatedAt.localeCompare(a.updatedAt);
}
return 0;
})
.filter(
?.filter(
(data) =>
data.name.includes(searchValue.toLocaleString()) ||
data.id === searchValue.toLocaleString(),
data?.name?.includes(searchValue.toLocaleString()) ||
data?.id.toLocaleString() === searchValue.toLocaleString(),
)
.map((data) => {
.map?.((data) => {
const specificAlertOptions = getAlertOptionsFromIds(
data.alertIds || [],
alertOptions,

View File

@@ -1,19 +1,21 @@
import { UseMutateAsyncFunction } from 'react-query';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { DefaultOptionType } from 'antd/es/select';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type {
DeleteDowntimeScheduleByIDPathParameters,
RenderErrorResponseDTO,
RuletypesPlannedMaintenanceDTO,
RuletypesRecurrenceDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { AxiosError } from 'axios';
import createDowntimeSchedule from 'api/plannedDowntime/createDowntimeSchedule';
import { DeleteSchedulePayloadProps } from 'api/plannedDowntime/deleteDowntimeSchedule';
import {
DowntimeSchedules,
Recurrence,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import updateDowntimeSchedule, {
DowntimeScheduleUpdatePayload,
PayloadProps,
} from 'api/plannedDowntime/updateDowntimeSchedule';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { isEmpty, isEqual } from 'lodash-es';
import APIError from 'types/api/error';
import { ErrorResponse, SuccessResponse } from 'types/api';
type DateTimeString = string | null | undefined;
@@ -59,21 +61,15 @@ export const getAlertOptionsFromIds = (
alertIds?.includes(alert.value as string),
);
export const recurrenceInfo = (
recurrence?: RuletypesRecurrenceDTO | null,
): string => {
export const recurrenceInfo = (recurrence?: Recurrence | null): string => {
if (!recurrence) {
return 'No';
}
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
const formattedStartTime = startTime
? formatDateTime(dayjs(startTime).toISOString())
: '';
const formattedEndTime = endTime
? `to ${formatDateTime(dayjs(endTime).toISOString())}`
: '';
const formattedStartTime = startTime ? formatDateTime(startTime) : '';
const formattedEndTime = endTime ? `to ${formatDateTime(endTime)}` : '';
const weeklyRepeatString = repeatOn ? `on ${repeatOn.join(', ')}` : '';
const durationString = duration ? `- Duration: ${duration}` : '';
@@ -81,32 +77,31 @@ export const recurrenceInfo = (
};
export const defautlInitialValues: Partial<
RuletypesPlannedMaintenanceDTO & { editMode: boolean }
DowntimeSchedules & { editMode: boolean }
> = {
name: '',
description: '',
schedule: {
timezone: '',
endTime: undefined,
recurrence: undefined,
startTime: undefined,
endTime: '',
recurrence: null,
startTime: '',
},
alertIds: [],
createdAt: undefined,
createdBy: undefined,
createdAt: '',
createdBy: '',
editMode: false,
};
type DeleteDowntimeScheduleProps = {
deleteDowntimeScheduleAsync: UseMutateAsyncFunction<
void,
ErrorType<RenderErrorResponseDTO>,
{ pathParams: DeleteDowntimeScheduleByIDPathParameters }
DeleteSchedulePayloadProps,
Error,
number
>;
notifications: NotificationInstance;
showErrorModal: (error: APIError) => void;
refetchAllSchedules: VoidFunction;
deleteId?: string;
deleteId?: number;
hideDeleteDowntimeScheduleModal: () => void;
clearSearch: () => void;
};
@@ -118,33 +113,40 @@ export const deleteDowntimeHandler = ({
hideDeleteDowntimeScheduleModal,
clearSearch,
notifications,
showErrorModal,
}: DeleteDowntimeScheduleProps): void => {
if (!deleteId) {
const errorMsg = new Error('Something went wrong');
console.error('Unable to delete, please provide correct deleteId');
notifications.error({ message: 'Something went wrong' });
showErrorNotification(notifications, errorMsg);
} else {
deleteDowntimeScheduleAsync(
{ pathParams: { id: String(deleteId) } },
{
onSuccess: () => {
hideDeleteDowntimeScheduleModal();
clearSearch();
notifications.success({
message: 'Downtime schedule Deleted Successfully',
});
refetchAllSchedules();
},
onError: (err) => {
showErrorModal(
convertToApiError(err as AxiosError<RenderErrorResponseDTO>) as APIError,
);
},
deleteDowntimeScheduleAsync(deleteId, {
onSuccess: () => {
hideDeleteDowntimeScheduleModal();
clearSearch();
notifications.success({
message: 'Downtime schedule Deleted Successfully',
});
refetchAllSchedules();
},
);
onError: (err) => {
showErrorNotification(notifications, err);
},
});
}
};
export const createEditDowntimeSchedule = async (
props: DowntimeScheduleUpdatePayload,
): Promise<
| SuccessResponse<PayloadProps>
| ErrorResponse<{ code: string; message: string } | string>
> => {
if (props.id) {
return updateDowntimeSchedule({ ...props });
}
return createDowntimeSchedule({ ...props.data });
};
export const recurrenceOptions = {
doesNotRepeat: {
label: 'Does not repeat',
@@ -228,21 +230,19 @@ export const getEndTime = ({
kind,
schedule,
}: Partial<
RuletypesPlannedMaintenanceDTO & {
DowntimeSchedules & {
editMode: boolean;
}
>): string | dayjs.Dayjs => {
if (kind === 'fixed') {
return schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '';
return schedule?.endTime || '';
}
return schedule?.recurrence?.endTime
? dayjs(schedule.recurrence.endTime).toISOString()
: '';
return schedule?.recurrence?.endTime || '';
};
export const isScheduleRecurring = (
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
schedule?: DowntimeSchedules['schedule'],
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {

View File

@@ -1,10 +1,7 @@
import { UseQueryResult } from 'react-query';
import { fireEvent, screen } from '@testing-library/react';
import type {
ListDowntimeSchedules200,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { ErrorType } from 'api/generatedAPIInstance';
import { PayloadProps } from 'api/plannedDowntime/getAllDowntimeSchedules';
import { AxiosError, AxiosResponse } from 'axios';
import {
mockLocation,
mockQueryParams,
@@ -25,53 +22,45 @@ const MOCK_DATE_2 = '2024-01-02';
const MOCK_DATE_3 = '2024-01-03';
const MOCK_DOWNTIME_1 = createMockDowntime({
id: '1',
id: 1,
name: MOCK_DOWNTIME_1_NAME,
createdAt: new Date(MOCK_DATE_1),
updatedAt: new Date(MOCK_DATE_1),
schedule: buildSchedule({
startTime: new Date(MOCK_DATE_1),
timezone: 'UTC',
}),
createdAt: MOCK_DATE_1,
updatedAt: MOCK_DATE_1,
schedule: buildSchedule({ startTime: MOCK_DATE_1, timezone: 'UTC' }),
alertIds: [],
});
const MOCK_DOWNTIME_2 = createMockDowntime({
id: '2',
id: 2,
name: MOCK_DOWNTIME_2_NAME,
createdAt: new Date(MOCK_DATE_2),
updatedAt: new Date(MOCK_DATE_2),
schedule: buildSchedule({
startTime: new Date(MOCK_DATE_2),
timezone: 'UTC',
}),
createdAt: MOCK_DATE_2,
updatedAt: MOCK_DATE_2,
schedule: buildSchedule({ startTime: MOCK_DATE_2, timezone: 'UTC' }),
alertIds: [],
});
const MOCK_DOWNTIME_3 = createMockDowntime({
id: '3',
id: 3,
name: MOCK_DOWNTIME_3_NAME,
createdAt: new Date(MOCK_DATE_3),
updatedAt: new Date(MOCK_DATE_3),
schedule: buildSchedule({
startTime: new Date(MOCK_DATE_3),
timezone: 'UTC',
}),
createdAt: MOCK_DATE_3,
updatedAt: MOCK_DATE_3,
schedule: buildSchedule({ startTime: MOCK_DATE_3, timezone: 'UTC' }),
alertIds: [],
});
const MOCK_DOWNTIME_RESPONSE: ListDowntimeSchedules200 = {
data: [MOCK_DOWNTIME_1, MOCK_DOWNTIME_2, MOCK_DOWNTIME_3],
status: 'success',
const MOCK_DOWNTIME_RESPONSE: Partial<AxiosResponse<PayloadProps>> = {
data: {
data: [MOCK_DOWNTIME_1, MOCK_DOWNTIME_2, MOCK_DOWNTIME_3],
},
};
type DowntimeQueryResult = UseQueryResult<
ListDowntimeSchedules200,
ErrorType<RenderErrorResponseDTO>
AxiosResponse<PayloadProps>,
AxiosError
>;
const mockDowntimeQueryResult: Partial<DowntimeQueryResult> = {
data: MOCK_DOWNTIME_RESPONSE,
data: MOCK_DOWNTIME_RESPONSE as AxiosResponse<PayloadProps>,
isLoading: false,
isFetching: false,
isError: false,
@@ -100,27 +89,13 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('api/generated/services/downtimeschedules', () => ({
useListDowntimeSchedules: (): DowntimeQueryResult =>
jest.mock('api/plannedDowntime/getAllDowntimeSchedules', () => ({
useGetAllDowntimeSchedules: (): DowntimeQueryResult =>
mockDowntimeQueryResult as DowntimeQueryResult,
useDeleteDowntimeScheduleByID: (): {
mutateAsync: jest.Mock;
isLoading: false;
} => ({
mutateAsync: jest.fn(),
isLoading: false,
}),
}));
jest.mock('api/generated/services/rules', () => ({
useListRules: (): {
data: { data: [] };
isError: false;
isLoading: false;
} => ({
data: { data: [] },
isError: false,
isLoading: false,
}),
jest.mock('api/alerts/getAll', () => ({
__esModule: true,
default: (): Promise<{ payload: [] }> => Promise.resolve({ payload: [] }),
}));
describe('PlannedDowntime Component', () => {

View File

@@ -1,37 +1,29 @@
import type {
RuletypesPlannedMaintenanceDTO,
RuletypesScheduleDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
RuletypesMaintenanceKindDTO,
RuletypesMaintenanceStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import { DowntimeSchedules } from 'api/plannedDowntime/getAllDowntimeSchedules';
export const buildSchedule = (
schedule: Partial<RuletypesScheduleDTO>,
): RuletypesScheduleDTO => ({
timezone: schedule?.timezone ?? '',
startTime: schedule?.startTime,
endTime: schedule?.endTime,
recurrence: schedule?.recurrence,
schedule: Partial<DowntimeSchedules['schedule']>,
): DowntimeSchedules['schedule'] => ({
timezone: schedule?.timezone ?? null,
startTime: schedule?.startTime ?? null,
endTime: schedule?.endTime ?? null,
recurrence: schedule?.recurrence ?? null,
});
export const createMockDowntime = (
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
): RuletypesPlannedMaintenanceDTO => ({
id: overrides.id ?? '0',
name: overrides.name ?? '',
description: overrides.description ?? '',
overrides: Partial<DowntimeSchedules>,
): DowntimeSchedules => ({
id: overrides.id ?? 0,
name: overrides.name ?? null,
description: overrides.description ?? null,
schedule: buildSchedule({
timezone: 'UTC',
startTime: new Date('2024-01-01'),
startTime: '2024-01-01',
...overrides.schedule,
}),
alertIds: overrides.alertIds ?? [],
createdAt: overrides.createdAt,
createdBy: overrides.createdBy ?? '',
updatedAt: overrides.updatedAt,
updatedBy: overrides.updatedBy ?? '',
kind: overrides.kind ?? RuletypesMaintenanceKindDTO.recurring,
status: overrides.status ?? RuletypesMaintenanceStatusDTO.active,
alertIds: overrides.alertIds ?? null,
createdAt: overrides.createdAt ?? null,
createdBy: overrides.createdBy ?? null,
updatedAt: overrides.updatedAt ?? null,
updatedBy: overrides.updatedBy ?? null,
kind: overrides.kind ?? null,
});

View File

@@ -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?.();
}}
>

View File

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

View File

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

View File

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

View File

@@ -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',
);

View File

@@ -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));
}

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