Compare commits

..

16 Commits

Author SHA1 Message Date
SagarRajput-7
71fd5910a4 Merge branch 'base-path-task-phase-2' into base-path-task-phase-3 2026-04-21 11:04:41 +05:30
SagarRajput-7
06e521ad97 Merge branch 'base-path-config-setup-1' into base-path-task-phase-2 2026-04-21 11:04:29 +05:30
SagarRajput-7
a9dcad863b Merge branch 'main' into base-path-config-setup-1 2026-04-21 11:04:19 +05:30
SagarRajput-7
44cb8f5927 feat(base-path): migrated the new files added after rebase with main 2026-04-21 10:56:19 +05:30
SagarRajput-7
c88c7bac0f feat(base-path): migrate backend bound urls and eslint upgrade to error 2026-04-21 10:39:42 +05:30
SagarRajput-7
72db77b068 feat(base-path): migrate remaining pattern for window.location.origin + path 2026-04-21 10:33:24 +05:30
SagarRajput-7
4f84f07494 feat(base-path): replace window.open with openInNewTab for internal paths 2026-04-21 10:28:57 +05:30
SagarRajput-7
a9e09ee349 feat: code refactor around feedbacks 2026-04-21 10:25:02 +05:30
SagarRajput-7
0a9bb6ba0b feat: applied suggested patch changes 2026-04-21 10:24:51 +05:30
SagarRajput-7
6664a0fae3 feat: code refactor around feedbacks 2026-04-21 10:24:38 +05:30
SagarRajput-7
040dcb9c9b feat: updated base path utils and fixed navigation and translations 2026-04-21 10:24:23 +05:30
SagarRajput-7
4a39453826 feat: updated the html template 2026-04-21 10:24:12 +05:30
SagarRajput-7
ac4db09ec6 feat: removed plugin and serving the index.html only as the template 2026-04-21 10:24:01 +05:30
SagarRajput-7
a691e6a775 feat: refactor the interceptor and added gotmpl into gitignore 2026-04-21 10:23:51 +05:30
SagarRajput-7
d270a3807b feat: changed output path to dir level 2026-04-21 10:23:42 +05:30
SagarRajput-7
c2b553d26c feat: base path config setup and plugin for gotmpl generation at build time 2026-04-21 10:23:33 +05:30
125 changed files with 1850 additions and 1098 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

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

@@ -51,6 +51,7 @@ import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { filterDuplicateFilters } from '../commonUtils';
@@ -569,10 +570,7 @@ function K8sBaseDetails<T>({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -591,10 +589,7 @@ function K8sBaseDetails<T>({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -4,6 +4,7 @@ import {
CloudintegrationtypesServiceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { withBasePath } from 'utils/basePath';
import './ServiceDashboards.styles.scss';
@@ -44,7 +45,11 @@ function ServiceDashboards({
return;
}
if (event.metaKey || event.ctrlKey) {
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
window.open(
withBasePath(dashboardUrl),
'_blank',
'noopener,noreferrer',
);
return;
}
safeNavigate(dashboardUrl);
@@ -54,7 +59,11 @@ function ServiceDashboards({
return;
}
if (event.button === 1) {
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
window.open(
withBasePath(dashboardUrl),
'_blank',
'noopener,noreferrer',
);
}
}}
onKeyDown={(event): void => {

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

View File

@@ -28,6 +28,7 @@ import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { openInNewTab } from 'utils/navigation';
import './TracesTableComponent.styles.scss';
@@ -86,7 +87,7 @@ function TracesTableComponent({
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
openInNewTab(getTraceLink(record));
} else {
history.push(getTraceLink(record));
}

View File

@@ -0,0 +1,20 @@
import { useMutation, UseMutationResult } from 'react-query';
import createAlertRule, {
CreateAlertRuleResponse,
} from 'api/alerts/createAlertRule';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
export function useCreateAlertRule(): UseMutationResult<
SuccessResponse<CreateAlertRuleResponse> | ErrorResponse,
Error,
PostableAlertRuleV2
> {
return useMutation<
SuccessResponse<CreateAlertRuleResponse> | ErrorResponse,
Error,
PostableAlertRuleV2
>({
mutationFn: (alertData) => createAlertRule(alertData),
});
}

View File

@@ -0,0 +1,18 @@
import { useMutation, UseMutationResult } from 'react-query';
import testAlertRule, { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
export function useTestAlertRule(): UseMutationResult<
SuccessResponse<TestAlertRuleResponse> | ErrorResponse,
Error,
PostableAlertRuleV2
> {
return useMutation<
SuccessResponse<TestAlertRuleResponse> | ErrorResponse,
Error,
PostableAlertRuleV2
>({
mutationFn: (alertData) => testAlertRule(alertData),
});
}

View File

@@ -0,0 +1,22 @@
import { useMutation, UseMutationResult } from 'react-query';
import updateAlertRule, {
UpdateAlertRuleResponse,
} from 'api/alerts/updateAlertRule';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
export function useUpdateAlertRule(
id: string,
): UseMutationResult<
SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse,
Error,
PostableAlertRuleV2
> {
return useMutation<
SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse,
Error,
PostableAlertRuleV2
>({
mutationFn: (alertData) => updateAlertRule(id, alertData),
});
}

View File

@@ -172,6 +172,7 @@ export function useIntegrationModal({
id: accountId,
},
);
// eslint-disable-next-line rulesdir/no-raw-absolute-path -- connectionUrl is an external AWS console URL, not an internal path
window.open(connectionUrl, '_blank');
setModalState(ModalStateEnum.WAITING);
setAccountId(accountId);

View File

@@ -17,6 +17,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getAbsoluteUrl } from 'utils/basePath';
import { HIGHLIGHTED_DELAY } from './configs';
import { UseCopyLogLink } from './types';
@@ -60,7 +61,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
setCopy(link);

View File

@@ -21,6 +21,7 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { withBasePath } from 'utils/basePath';
import { getGraphType } from 'utils/getGraphType';
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
@@ -92,7 +93,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const url = `${ROUTES.ALERTS_NEW}?${params.toString()}`;
window.open(url, '_blank', 'noreferrer');
window.open(withBasePath(url), '_blank', 'noreferrer');
},
onError: () => {
notifications.error({

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