Compare commits

..

42 Commits

Author SHA1 Message Date
srikanthccv
a1989e9c92 chore: parity with expected enum values of match type and compare operator 2026-04-21 22:05:32 +05:30
Srikanth Chekuri
eb299ce885 Merge branch 'main' into platform-pod/issues/2095-frontend 2026-04-21 11:52:19 +05:30
grandwizard28
1856022fb1 refactor(frontend): inline single-use handleMutationError in alert hooks
Replaces four call sites in AlertDetails/hooks.tsx with the same
showErrorModal(convertToApiError(...)) expression directly; the helper
added no semantics beyond the type cast it wrapped. The two pure
onError arrows end up byte-identical, so the delete hook carries an
eslint-disable for sonarjs/no-identical-functions (same pattern already
used in providers/QueryBuilder.tsx).
2026-04-20 14:33:32 +05:30
grandwizard28
b8af290891 refactor(frontend): replace inline alert DTO casts with typed converters 2026-04-20 14:33:32 +05:30
grandwizard28
540b05c111 refactor(frontend): add patchRulePartial wrapper for Partial rule patches 2026-04-20 14:33:32 +05:30
grandwizard28
2edbede3b9 refactor(frontend): add typed converters between local alert types and generated DTOs 2026-04-20 14:33:32 +05:30
grandwizard28
0563b50d92 chore(frontend): stop logging validateFields rejection from downtime form
Form.validateFields() rejects with {values, errorFields, outOfDate}
whenever a required field is empty (e.g. Ends on). antd already renders
the field-level error inline, so console.error adds no user-facing
value and pollutes Sentry / devtools with routine typos.

Replaces the try/catch+log with a noop .catch().
2026-04-20 14:33:31 +05:30
grandwizard28
ab27055579 fix(frontend): re-hydrate downtime edit drawer when alerts list arrives async
The formatedInitialValues memo depended only on initialValues, so when the
edit drawer opened before useListRules resolved, getAlertOptionsFromIds
returned an empty array and the form's alertRules state stayed empty —
even though the sidecar AlertRuleTags rendered a tag via selectedTags.
Submitting then sent alertIds: [] or stale values, which is why the
"edit downtime → remove silenced alert → save → delete" cleanup flow
never actually cleared the server-side association.

Adds alertOptions to the deps so the memo recomputes once the rules
list arrives, letting the useEffect below re-hydrate both selectedTags
and the form state with the correct tags.
2026-04-20 14:33:31 +05:30
grandwizard28
c37e37b156 refactor(frontend): adopt renamed RuletypesRuleDTO and created/updated field names 2026-04-20 14:33:31 +05:30
grandwizard28
17f40824fb refactor(frontend): surface BE error via convertToApiError on save alert paths 2026-04-20 14:33:31 +05:30
grandwizard28
8aa1711c9d refactor(frontend): align defautlInitialValues createdBy with createdAt as undefined 2026-04-20 14:33:31 +05:30
grandwizard28
0fdadd806f refactor(frontend): add dataSourceForAlertType helper keyed on generated enum 2026-04-20 14:33:31 +05:30
grandwizard28
d3edae314e refactor(frontend): drop as unknown from copyAlert cast in clone flow 2026-04-20 14:33:31 +05:30
grandwizard28
33f6b4609e refactor(frontend): type defautlInitialValues as Partial directly 2026-04-20 14:33:30 +05:30
grandwizard28
fb7aee1ad7 refactor(frontend): type formData seed as Partial in PlannedDowntimeForm 2026-04-20 14:33:30 +05:30
grandwizard28
aec75105c8 refactor(frontend): drop Record cast in PlannedDowntime recurrence spread 2026-04-20 14:33:30 +05:30
grandwizard28
61ec46ca60 refactor(frontend): drop optional chains on fields now required by generated types 2026-04-20 14:33:30 +05:30
grandwizard28
8c5364965f refactor(frontend): drop duplicative AlertDetailsStatusRenderer
The inner component re-derived alertRuleDetails from the same response
as the parent, but via the stale .payload.data path that didn't exist
on the generated GetRuleByID200 shape — so it was silently feeding
undefined to AlertHeader. The any-typed data prop hid the error from
TypeScript during the migration to generated hooks.

The parent already derives alertRuleDetails correctly and gates on
isError/isLoading before the main render, so the inner component's
loading/error branches were unreachable. Inline AlertHeader directly.
2026-04-20 14:33:30 +05:30
grandwizard28
d1656c9ba4 refactor(frontend): show backend error message directly on rule-fetch failure
Drop the improvedErrorMessage override that hid the backend's message
behind a generic "does not exist" string. With the backend now
returning a descriptive "rule with ID: <id> does not exist" on 404, the
APIError message is the right thing to render. Inline the remaining
button label and delete the now-empty constants module.
2026-04-20 14:33:29 +05:30
grandwizard28
c311b44a59 refactor(frontend): key the alert-not-found message on HTTP 404
Drop the brittle comparison against the raw Go "sql: no rows in result
set" string. With the backend now wrapping sql.ErrNoRows as
TypeNotFound, GET /api/v1/rules/{id} returns HTTP 404 on a missing ID
(matching the existing OpenAPI contract). Use APIError.getHttpStatusCode
instead, and delete errorMessageReceivedFromBackend.
2026-04-20 14:33:29 +05:30
grandwizard28
28f710f34a refactor(frontend): cast timezone to string instead of empty-string default
Antd's required validation on the timezone field blocks submission when
empty, so values.timezone is guaranteed non-undefined by the time
saveHanlder runs. `?? ''` silently substituted a value the backend
rejects; `as string` matches the existing pattern used for the same
field at initialization (line 299) and surfaces drift as a type error
instead of a 400.
2026-04-20 14:33:29 +05:30
grandwizard28
49e016dbd8 fix(frontend): satisfy newly-required condition and timezone fields
After merging the ruler schema tightening, RuletypesRuleConditionDTO
now requires compositeQuery/op/matchType, AlertCompositeQuery requires
queries/panelType/queryType, and Schedule requires timezone. Update
the filterAlerts test fixtures to populate the nested condition, and
default timezone to an empty string in the downtime form payload so
the generated type is satisfied (backend still validates timezone is
non-empty).
2026-04-20 14:33:29 +05:30
grandwizard28
cfd464deaa refactor(frontend): retire GettableAlert in favor of generated type
Status and AlertHeader consume RuletypesGettableRuleDTO directly from
the generated OpenAPI client. Delete types/api/alerts/{get,getAll,patch}.ts
now that nothing imports from them.
2026-04-20 14:33:29 +05:30
grandwizard28
f7dcefd066 test(frontend): satisfy newly-required condition/ruleType in filterAlerts mocks
After merging the backend schema tightening, RuletypesGettableRuleDTO
requires `condition` and `ruleType`. Update the test fixtures to
include placeholder values so they type-check against the stricter
shape.
2026-04-20 14:33:29 +05:30
grandwizard28
9e518db298 refactor(frontend): adopt generated rule types, drop as any casts
Switch reader components (ListAlertRules, Home/AlertRules, AlertDetails,
EditRules) to use RuletypesGettableRuleDTO directly from the generated
OpenAPI client. Writer callers cast once via `as unknown as
RuletypesPostableRuleDTO` at the API boundary. Also fixes a poll bug in
ListAlert.tsx that read the old refetchData.payload shape.
2026-04-20 14:33:28 +05:30
grandwizard28
8aae7871ae refactor(frontend): adopt IngestionSettings date pattern in PlannedDowntime
Replace all (x as unknown) as Date/string double casts with the
established codebase pattern: new Date() for writes to API, dayjs()
for reads. Removes String() wraps in PlannedDowntimeList in favor of
dayjs().toISOString(). Tests use new Date() instead of double casts.
2026-04-20 14:33:28 +05:30
grandwizard28
a7b68a37ca fix(frontend): mock useErrorModal in Footer test to fix provider error
Footer now calls useErrorModal which requires ErrorModalProvider. Add
mock to the test since it imports render from @testing-library/react
directly instead of the custom test-utils wrapper.
2026-04-20 14:33:28 +05:30
grandwizard28
4812195c19 fix(frontend): update PlannedDowntime test mock to use generated rules hook
Replace deleted api/alerts/getAll mock with api/generated/services/rules
mock returning useListRules shape.
2026-04-20 14:33:28 +05:30
grandwizard28
091e860c45 fix(frontend): handle null rules from API to prevent infinite spinner
The generated schema allows rules?: ...[] | null. When the API returns
null, the component treated it as still-loading. Normalize null to []
via nullish coalescing and gate loading on data presence, not rules
truthiness.
2026-04-20 14:33:28 +05:30
grandwizard28
023db73e2c fix(frontend): remove downtime ID number coercion, delete routing adapter
Delete DowntimeScheduleUpdatePayload and createEditDowntimeSchedule —
the form now calls createDowntimeSchedule/updateDowntimeScheduleByID
directly, keeping the string ID from the generated schema end-to-end
instead of coercing to number.
2026-04-20 14:33:27 +05:30
grandwizard28
ab3bcfe572 refactor(frontend): integrate convertToApiError and error modal across rules/downtime
Replace all raw (error as any)?.message casts and generic "Something
went wrong" strings with proper error handling using convertToApiError
and showErrorModal. Query errors use convertToApiError for message
extraction, mutation errors show the rich error modal.
2026-04-20 14:33:27 +05:30
grandwizard28
830a4a2768 refactor(frontend): migrate updateAlertRule to generated useUpdateRuleByID
Delete hooks/alerts/useUpdateAlertRule.ts and api/alerts/updateAlertRule.ts.
Migrate CreateAlertV2 context to use useUpdateRuleByID from generated
services. Expose ruleId on context so Footer can pass it to the mutation.
2026-04-20 14:33:27 +05:30
grandwizard28
170c7006e7 refactor(frontend): delete dead API adapters, migrate save to generated hooks
Delete 14 unused hand-written API adapter files for alerts and planned
downtime. Migrate the 3 consumers of save.ts to call createRule and
updateRuleByID from generated services directly, removing the
create/update routing layer.
2026-04-20 14:33:27 +05:30
grandwizard28
e9b3003727 refactor(frontend): delete dead re-export wrappers for rules hooks
Remove api/alerts/getAll.ts, hooks/alerts/useCreateAlertRule.ts, and
hooks/alerts/useTestAlertRule.ts — all were single-line re-exports of
generated hooks with zero unique logic. Update the one consumer
(CreateAlertV2 context) and its test to import directly from the
generated services.
2026-04-20 14:33:27 +05:30
grandwizard28
91d0c287ba fix(frontend): fix accidental rename, cache fragmentation, and sort mutation
- Revert setRuletypesRecurrenceDTOType back to setRecurrenceType (find-replace artifact)
- Remove duplicate isError condition in AlertDetails guard
- Replace manual useQuery(listRules) with useListRules in PlannedDowntime and hooks
- Fix showErrorNotification import path in PlannedDowntimeutils
- Use spread before sort to avoid mutating react-query cache
2026-04-20 14:33:27 +05:30
grandwizard28
f49bfbdfcf refactor(frontend): switch to generated react-query hooks directly
Replace adapter wrappers with direct usage of generated hooks:
- useListRules() in ListAlertRules, AlertRules
- useGetRuleByID() in AlertDetails hooks, EditRules
- useCreateRule re-exported as useCreateAlertRule
- useTestRule re-exported as useTestAlertRule
- Update consumers to access data.data instead of data.payload
- Remove SuccessResponse envelope wrappers
2026-04-20 14:33:27 +05:30
grandwizard28
5d804ad9a5 refactor(frontend): extract getAll adapter to shared api/alerts/getAll.ts
Remove duplicated inline getAll adapter from ListAlertRules and
AlertRules components. Replace the old hand-written getAll.ts with a
thin adapter over the generated listRules client.
2026-04-20 14:33:27 +05:30
grandwizard28
3e576992f2 refactor(frontend): migrate rules APIs to generated clients
Replace hand-written getAll, get, delete, patch imports with generated
listRules, getRuleByID, deleteRuleByID, patchRuleByID from Orval
clients. Use adapter wrappers in consumers that depend on the old
SuccessResponse envelope to minimize cascading type changes.
2026-04-20 14:33:27 +05:30
grandwizard28
dd27f32d44 refactor(frontend): migrate createAlertRule API to generated createRule client
Update useCreateAlertRule hook to use generated createRule function.
Maintain backward-compatible SuccessResponse wrapper for CreateAlertV2
context consumers.
2026-04-20 14:33:26 +05:30
grandwizard28
e30d5ca0af refactor(frontend): migrate testAlert API to generated testRule client
Replace direct testAlertApi call in FormAlertRules with generated
testRule function. Update useTestAlertRule hook to use generated client
while maintaining backward-compatible wrapper types for CreateAlertV2
context consumers.
2026-04-20 14:33:26 +05:30
grandwizard28
4a9225d068 refactor(frontend): migrate all downtime schedule APIs to generated clients
Replace hand-written getAllDowntimeSchedules, createDowntimeSchedule,
updateDowntimeSchedule with generated useListDowntimeSchedules,
createDowntimeSchedule, updateDowntimeScheduleByID from Orval clients.

Update all type references from DowntimeSchedules/Recurrence to
RuletypesGettablePlannedMaintenanceDTO/RuletypesRecurrenceDTO.
Update tests to mock generated hooks.
2026-04-20 14:33:26 +05:30
grandwizard28
71f3d1889c refactor(frontend): migrate deleteDowntimeSchedule to generated client
Replace useDeleteDowntimeSchedule with useDeleteDowntimeScheduleByID
from the generated API client. Update PlannedDowntime.tsx and
PlannedDowntimeutils.ts to use pathParams pattern.
2026-04-20 14:33:26 +05:30
159 changed files with 1470 additions and 2142 deletions

View File

@@ -66,8 +66,6 @@ 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
@@ -215,31 +213,6 @@ module.exports = {
message:
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
},
{
selector:
"MemberExpression[object.name='window'][property.name='localStorage']",
message:
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
},
{
selector:
"MemberExpression[object.name='window'][property.name='sessionStorage']",
message:
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
},
],
'no-restricted-globals': [
'error',
{
name: 'localStorage',
message:
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
},
{
name: 'sessionStorage',
message:
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
},
],
},
overrides: [
@@ -273,11 +246,6 @@ module.exports = {
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
// Test assertions intentionally reference window.location.origin for expected-value checks
'rulesdir/no-raw-absolute-path': 'off',
// Tests may access storage directly for setup/assertion/spy purposes
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
},
},
{

View File

@@ -1,153 +0,0 @@
'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,7 +2,6 @@
<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"
@@ -40,12 +39,12 @@
<meta
data-react-helmet="true"
property="og:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
@@ -60,7 +59,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>
@@ -137,7 +136,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,7 +2,6 @@ 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';
@@ -25,7 +24,7 @@ i18n
const ns = namespace[0];
const pathkey = `/${language}/${ns}`;
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
return `${getBasePath()}locales/${language}/${namespace}.json?h=${hash}`;
return `/locales/${language}/${namespace}.json?h=${hash}`;
},
},
react: {

View File

@@ -1,20 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,20 +0,0 @@
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

@@ -0,0 +1,12 @@
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

@@ -1,20 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,26 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,26 +0,0 @@
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,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
return localStorage.getItem(getScopedKey(key));
return localStorage.getItem(key);
} catch (e) {
return '';
}

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
localStorage.removeItem(getScopedKey(key));
window.localStorage.removeItem(key);
return true;
} catch (e) {
return false;

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
localStorage.setItem(getScopedKey(key), value);
localStorage.setItem(key, value);
return true;
} catch (e) {
return false;

View File

@@ -1,12 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
return sessionStorage.getItem(getScopedKey(key));
} catch (e) {
return '';
}
};
export default get;

View File

@@ -1,13 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
sessionStorage.removeItem(getScopedKey(key));
return true;
} catch (e) {
return false;
}
};
export default remove;

View File

@@ -1,13 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
sessionStorage.setItem(getScopedKey(key), value);
return true;
} catch (e) {
return false;
}
};
export default set;

View File

@@ -1,6 +1,5 @@
import {
interceptorRejected,
interceptorsRequestBasePath,
interceptorsRequestResponse,
interceptorsResponse,
} from 'api';
@@ -18,7 +17,6 @@ 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,7 +11,6 @@ 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';
@@ -68,39 +67,6 @@ 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>> => {
@@ -167,7 +133,6 @@ 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({
@@ -182,7 +147,6 @@ ApiV2Instance.interceptors.response.use(
interceptorRejected,
);
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV2Instance.interceptors.request.use(interceptorsRequestBasePath);
// axios V3
export const ApiV3Instance = axios.create({
@@ -194,7 +158,6 @@ ApiV3Instance.interceptors.response.use(
interceptorRejected,
);
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV3Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V4
@@ -207,7 +170,6 @@ ApiV4Instance.interceptors.response.use(
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV4Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V5
@@ -220,7 +182,6 @@ ApiV5Instance.interceptors.response.use(
interceptorRejected,
);
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV5Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios Base
@@ -233,7 +194,6 @@ LogEventAxiosInstance.interceptors.response.use(
interceptorRejectedBase,
);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
//
AxiosAlertManagerInstance.interceptors.response.use(
@@ -241,7 +201,6 @@ AxiosAlertManagerInstance.interceptors.response.use(
interceptorRejected,
);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestResponse);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestBasePath);
export { apiV1 };
export default instance;

View File

@@ -3,16 +3,13 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { withBasePath } from 'utils/basePath';
// 10 min in ms
const TIMEOUT_IN_MS = 10 * 60 * 1000;
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
new EventSourcePolyfill(
ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
{
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,

View File

@@ -1,45 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,51 +0,0 @@
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

@@ -1,37 +0,0 @@
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,7 +12,6 @@ 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[];
@@ -134,7 +133,7 @@ export function useNavigateToExplorer(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
},
[
prepareQuery,

View File

@@ -9,7 +9,6 @@ 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();
@@ -55,7 +54,7 @@ export default function ChatSupportGateway(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

@@ -31,7 +31,6 @@ 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';
@@ -388,7 +387,7 @@ function EditMemberDrawer({
pathParams: { id: member.id },
});
if (response?.data?.token) {
const link = getAbsoluteUrl(`/password-reset?token=${response.data.token}`);
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
setResetLink(link);
setResetLinkExpiresAt(
response.data.expiresAt

View File

@@ -13,7 +13,6 @@ 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,
@@ -81,13 +80,17 @@ function ShareURLModal(): JSX.Element {
urlQuery.delete(QueryParams.relativeTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
} else {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
}
}

View File

@@ -14,7 +14,6 @@ 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';
@@ -189,7 +188,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role as ROLES,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
});
} else {
await inviteUsers({
@@ -197,7 +196,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
})),
});
}

View File

@@ -14,7 +14,6 @@ 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';
@@ -155,7 +154,7 @@ function LaunchChatSupport({
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

@@ -1,7 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ArrowUpRight } from 'lucide-react';
import { openInNewTab } from 'utils/navigation';
import './LearnMore.styles.scss';
@@ -15,7 +14,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
const handleClick = (): void => {
onClick?.();
if (url) {
openInNewTab(url);
window.open(url, '_blank');
}
};
return (

View File

@@ -20,7 +20,6 @@ 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';
@@ -77,7 +76,7 @@ function ErrorTitleAndKey({
if (isCloudUserVal && !!link) {
history.push(link);
} else {
openInNewTab(KAFKA_SETUP_DOC_LINK);
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return {

View File

@@ -1,13 +1,10 @@
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
import setSessionStorageApi from 'api/browser/sessionstorage/set';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const PREVIOUS_QUERY_KEY = 'previousQuery';
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
try {
const raw = getSessionStorageApi(PREVIOUS_QUERY_KEY);
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
if (!raw) {
return {};
}
@@ -20,7 +17,7 @@ function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
try {
setSessionStorageApi(PREVIOUS_QUERY_KEY, JSON.stringify(store));
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
} catch {
// ignore quota or serialization errors
}
@@ -66,7 +63,7 @@ export const removeKeyFromPreviousQuery = (key: string): void => {
export const clearPreviousQuery = (): void => {
try {
removeSessionStorageApi(PREVIOUS_QUERY_KEY);
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
} catch {
// no-op
}

View File

@@ -108,7 +108,8 @@ function DynamicColumnTable({
// Update URL with new page number while preserving other params
urlQuery.set('page', page.toString());
safeNavigate({ search: `?${urlQuery.toString()}` });
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
safeNavigate(newUrl);
// Call original pagination handler if provided
if (pagination?.onChange && !!pageSize) {

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { DynamicColumnsKey } from './contants';
import {
GetNewColumnDataFunction,
@@ -15,7 +12,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
}) => {
let columnVisibilityData: { [key: string]: boolean };
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
columnVisibilityData = JSON.parse(storedData);
return dynamicColumns.filter((column) => {
@@ -31,7 +28,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
initialColumnVisibility[key] = false;
});
setLocalStorageKey(tablesource, JSON.stringify(initialColumnVisibility));
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
} catch (error) {
console.error(error);
}
@@ -45,14 +42,14 @@ export const setVisibleColumns = ({
dynamicColumns,
}: SetVisibleColumnsProps): void => {
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
const columnVisibilityData = JSON.parse(storedData);
const { key } = dynamicColumns[index];
if (key) {
columnVisibilityData[key] = checked;
}
setLocalStorageKey(tablesource, JSON.stringify(columnVisibilityData));
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
}
} catch (error) {
console.error(error);

View File

@@ -1,3 +1,4 @@
import { RuletypesAlertTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { DataSource } from 'types/common/queryBuilder';
@@ -8,3 +9,17 @@ 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,7 +9,6 @@ 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';
@@ -95,14 +94,20 @@ function DependentServices({
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
const serviceName =
record.serviceData.serviceName && record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: '';
const url = new URL(
`/services/${
record.serviceData.serviceName &&
record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: ''
}`,
window.location.origin,
);
const urlQuery = new URLSearchParams();
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
openInNewTab(`/services/${serviceName}?${urlQuery.toString()}`);
url.search = urlQuery.toString();
window.open(url.toString(), '_blank');
},
className: 'clickable-row',
})}

View File

@@ -73,7 +73,6 @@ 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 {
@@ -462,7 +461,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const handleFailedPayment = useCallback((): void => {
manageCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}, [manageCreditCard]);

View File

@@ -31,7 +31,6 @@ 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';
@@ -325,7 +324,7 @@ export default function BillingContainer(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
} else {
logEvent('Billing : Manage Billing', {
@@ -334,7 +333,7 @@ export default function BillingContainer(): JSX.Element {
});
manageCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -7,7 +7,6 @@ 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';
@@ -56,7 +55,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
page: 'New alert data source selection page',
});
openInNewTab(url);
window.open(url, '_blank');
}
const renderOptions = useMemo(
() => (

View File

@@ -15,8 +15,12 @@ import {
THRESHOLD_MATCH_TYPE_OPTIONS,
THRESHOLD_OPERATOR_OPTIONS,
} from '../context/constants';
import { AlertThresholdMatchType } from '../context/types';
import {
AlertThresholdMatchType,
AlertThresholdOperator,
} from '../context/types';
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
import { normalizeMatchType, normalizeOperator } from '../utils';
import ThresholdItem from './ThresholdItem';
import { AnomalyAndThresholdProps, UpdateThreshold } from './types';
import {
@@ -132,12 +136,15 @@ function AlertThreshold({
}
};
const normalizedOperator =
normalizeOperator(thresholdState.operator) ?? AlertThresholdOperator.IS_ABOVE;
const matchTypeOptionsWithTooltips = THRESHOLD_MATCH_TYPE_OPTIONS.map(
(option) => ({
...option,
label: (
<Tooltip
title={getMatchTypeTooltip(option.value, thresholdState.operator)}
title={getMatchTypeTooltip(option.value, normalizedOperator)}
placement="left"
overlayClassName="copyable-tooltip"
overlayStyle={{
@@ -232,7 +239,10 @@ function AlertThreshold({
/>
<Typography.Text className="sentence-text">is</Typography.Text>
<Select
value={thresholdState.operator}
value={
(normalizeOperator(thresholdState.operator) ??
thresholdState.operator) as AlertThresholdOperator
}
onChange={(value): void => {
setThresholdState({
type: 'SET_OPERATOR',
@@ -247,7 +257,10 @@ function AlertThreshold({
the threshold(s)
</Typography.Text>
<Select
value={thresholdState.matchType}
value={
(normalizeMatchType(thresholdState.matchType) ??
thresholdState.matchType) as AlertThresholdMatchType
}
onChange={(value): void => {
setThresholdState({
type: 'SET_MATCH_TYPE',

View File

@@ -11,6 +11,11 @@ import {
ANOMALY_THRESHOLD_OPERATOR_OPTIONS,
ANOMALY_TIME_DURATION_OPTIONS,
} from '../context/constants';
import {
AlertThresholdMatchType,
AlertThresholdOperator,
} from '../context/types';
import { normalizeMatchType, normalizeOperator } from '../utils';
import { AnomalyAndThresholdProps } from './types';
import {
getQueryNames,
@@ -115,7 +120,10 @@ function AnomalyThreshold({
deviations
</Typography.Text>
<Select
value={thresholdState.operator}
value={
(normalizeOperator(thresholdState.operator) ??
thresholdState.operator) as AlertThresholdOperator
}
data-testid="operator-select"
onChange={(value): void => {
setThresholdState({
@@ -132,7 +140,10 @@ function AnomalyThreshold({
the predicted data
</Typography.Text>
<Select
value={thresholdState.matchType}
value={
(normalizeMatchType(thresholdState.matchType) ??
thresholdState.matchType) as AlertThresholdMatchType
}
data-testid="match-type-select"
onChange={(value): void => {
setThresholdState({

View File

@@ -5,6 +5,7 @@ import { useAppContext } from 'providers/App/App';
import { useCreateAlertState } from '../context';
import { AlertThresholdOperator } from '../context/types';
import { normalizeOperator } from '../utils';
import { ThresholdItemProps } from './types';
import { NotificationChannelsNotFoundContent } from './utils';
@@ -54,7 +55,7 @@ function ThresholdItem({
}, [units, threshold.unit, updateThreshold, threshold.id]);
const getOperatorSymbol = (): string => {
switch (thresholdState.operator) {
switch (normalizeOperator(thresholdState.operator)) {
case AlertThresholdOperator.IS_ABOVE:
return '>';
case AlertThresholdOperator.IS_BELOW:

View File

@@ -14,7 +14,6 @@ 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';
@@ -388,7 +387,7 @@ export function NotificationChannelsNotFoundContent({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
openInNewTab(ROUTES.CHANNELS_NEW);
window.open(ROUTES.CHANNELS_NEW, '_blank');
}}
>
here.

View File

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

View File

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

View File

@@ -1,9 +1,15 @@
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';
@@ -30,9 +36,20 @@ 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();
@@ -71,20 +88,21 @@ function Footer(): JSX.Element {
notificationSettings,
query: currentQuery,
});
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');
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,
},
onError: (error) => {
toast.error(error.message);
},
});
);
}, [
alertType,
basicAlertState,
@@ -107,25 +125,30 @@ function Footer(): JSX.Element {
query: currentQuery,
});
if (isEditMode) {
updateAlertRule(payload, {
onSuccess: () => {
toast.success('Alert rule updated successfully');
safeNavigate('/alerts');
updateAlertRule(
{
pathParams: { id: ruleId },
data: toPostableRuleDTO(payload),
},
onError: (error) => {
toast.error(error.message);
{
onSuccess: () => {
toast.success('Alert rule updated successfully');
safeNavigate('/alerts');
},
onError: handleApiError,
},
});
);
} else {
createAlertRule(payload, {
onSuccess: () => {
toast.success('Alert rule created successfully');
safeNavigate('/alerts');
createAlertRule(
{ data: toPostableRuleDTO(payload) },
{
onSuccess: () => {
toast.success('Alert rule created successfully');
safeNavigate('/alerts');
},
onError: handleApiError,
},
onError: (error) => {
toast.error(error.message);
},
});
);
}
}, [
alertType,
@@ -136,9 +159,11 @@ function Footer(): JSX.Element {
notificationSettings,
currentQuery,
isEditMode,
ruleId,
updateAlertRule,
createAlertRule,
safeNavigate,
handleApiError,
]);
const disableButtons =

View File

@@ -12,6 +12,11 @@ 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

@@ -474,9 +474,9 @@ describe('Footer utils', () => {
spec: [
{
channels: [],
matchType: '1',
matchType: 'at_least_once',
name: 'critical',
op: '1',
op: 'above',
target: 0,
targetUnit: '',
},
@@ -520,5 +520,33 @@ describe('Footer utils', () => {
expect(props.condition.compositeQuery.queryType).toBe('promql');
expect(props.ruleType).toBe('promql_rule');
});
// Backward compatibility: a rule loaded with a legacy op/matchType
// serialization ("1", ">", "eq", "above_or_equal", ...) must round-trip
// back to the backend unchanged when the user hasn't touched those
// fields. If the submit payload silently rewrites them, existing rules
// would drift away from their persisted form on every edit.
it.each([
['numeric', '1', '1'],
['symbol', '>', 'at_least_once'],
['literal', 'above', 'at_least_once'],
['short', 'eq', 'avg'],
['UI-unexposed', 'above_or_equal', 'at_least_once'],
])(
'round-trips %s op/matchType unchanged through the submit payload (%s / %s)',
(_desc, op, matchType) => {
const args: BuildCreateAlertRulePayloadArgs = {
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS,
thresholdState: {
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS.thresholdState,
operator: op,
matchType,
},
};
const props = buildCreateThresholdAlertRulePayload(args);
expect(props.condition.thresholds?.spec[0].op).toBe(op);
expect(props.condition.thresholds?.spec[0].matchType).toBe(matchType);
},
);
});
});

View File

@@ -15,6 +15,8 @@ import {
getEvaluationWindowStateFromAlertDef,
getNotificationSettingsStateFromAlertDef,
getThresholdStateFromAlertDef,
normalizeMatchType,
normalizeOperator,
parseGoTime,
} from '../utils';
@@ -314,6 +316,137 @@ describe('CreateAlertV2 utils', () => {
});
});
describe('normalizeOperator', () => {
it.each([
['1', AlertThresholdOperator.IS_ABOVE],
['above', AlertThresholdOperator.IS_ABOVE],
['>', AlertThresholdOperator.IS_ABOVE],
['2', AlertThresholdOperator.IS_BELOW],
['below', AlertThresholdOperator.IS_BELOW],
['<', AlertThresholdOperator.IS_BELOW],
['3', AlertThresholdOperator.IS_EQUAL_TO],
['equal', AlertThresholdOperator.IS_EQUAL_TO],
['eq', AlertThresholdOperator.IS_EQUAL_TO],
['=', AlertThresholdOperator.IS_EQUAL_TO],
['4', AlertThresholdOperator.IS_NOT_EQUAL_TO],
['not_equal', AlertThresholdOperator.IS_NOT_EQUAL_TO],
['not_eq', AlertThresholdOperator.IS_NOT_EQUAL_TO],
['!=', AlertThresholdOperator.IS_NOT_EQUAL_TO],
['7', AlertThresholdOperator.ABOVE_BELOW],
['outside_bounds', AlertThresholdOperator.ABOVE_BELOW],
])('maps backend alias %s to canonical enum', (alias, expected) => {
expect(normalizeOperator(alias)).toBe(expected);
});
it.each([
['5', 'above_or_equal'],
['above_or_equal', 'above_or_equal'],
['above_or_eq', 'above_or_equal'],
['>=', 'above_or_equal'],
['6', 'below_or_equal'],
['below_or_equal', 'below_or_equal'],
['below_or_eq', 'below_or_equal'],
['<=', 'below_or_equal'],
])('returns undefined for UI-unexposed alias %s (%s family)', (alias) => {
expect(normalizeOperator(alias)).toBeUndefined();
});
it('returns undefined for unknown values', () => {
expect(normalizeOperator('gibberish')).toBeUndefined();
expect(normalizeOperator(undefined)).toBeUndefined();
expect(normalizeOperator('')).toBeUndefined();
});
});
describe('normalizeMatchType', () => {
it.each([
['1', AlertThresholdMatchType.AT_LEAST_ONCE],
['at_least_once', AlertThresholdMatchType.AT_LEAST_ONCE],
['2', AlertThresholdMatchType.ALL_THE_TIME],
['all_the_times', AlertThresholdMatchType.ALL_THE_TIME],
['3', AlertThresholdMatchType.ON_AVERAGE],
['on_average', AlertThresholdMatchType.ON_AVERAGE],
['avg', AlertThresholdMatchType.ON_AVERAGE],
['4', AlertThresholdMatchType.IN_TOTAL],
['in_total', AlertThresholdMatchType.IN_TOTAL],
['sum', AlertThresholdMatchType.IN_TOTAL],
['5', AlertThresholdMatchType.LAST],
['last', AlertThresholdMatchType.LAST],
])('maps backend alias %s to canonical enum', (alias, expected) => {
expect(normalizeMatchType(alias)).toBe(expected);
});
it('returns undefined for unknown values', () => {
expect(normalizeMatchType('gibberish')).toBeUndefined();
expect(normalizeMatchType(undefined)).toBeUndefined();
expect(normalizeMatchType('')).toBeUndefined();
});
});
describe('getThresholdStateFromAlertDef backward compatibility', () => {
const buildDef = (op: string, matchType: string): PostableAlertRuleV2 => ({
...defaultPostableAlertRuleV2,
condition: {
...defaultPostableAlertRuleV2.condition,
thresholds: {
kind: 'basic',
spec: [
{
name: 'critical',
target: 1,
targetUnit: UniversalYAxisUnit.MINUTES,
channels: [],
matchType,
op,
},
],
},
},
});
// Each row covers a distinct historical serialization shape the backend
// may have persisted. The frontend must not rewrite these on load —
// otherwise opening and saving an old rule silently changes its op.
it.each([
['numeric', '1', '1'],
['literal', 'above', 'at_least_once'],
['symbol', '>', 'at_least_once'],
['short form', 'eq', 'avg'],
['mixed numeric and literal', '7', 'last'],
['UI-unexposed operator', 'above_or_equal', 'at_least_once'],
['UI-unexposed numeric operator', '5', 'at_least_once'],
])('preserves %s op/matchType verbatim (%s / %s)', (_desc, op, matchType) => {
const state = getThresholdStateFromAlertDef(buildDef(op, matchType));
expect(state.operator).toBe(op);
expect(state.matchType).toBe(matchType);
});
it('falls back to IS_ABOVE / AT_LEAST_ONCE when op and matchType are missing', () => {
const def: PostableAlertRuleV2 = {
...defaultPostableAlertRuleV2,
condition: {
...defaultPostableAlertRuleV2.condition,
thresholds: {
kind: 'basic',
spec: [
{
name: 'critical',
target: 1,
targetUnit: UniversalYAxisUnit.MINUTES,
channels: [],
matchType: '',
op: '',
},
],
},
},
};
const state = getThresholdStateFromAlertDef(def);
expect(state.operator).toBe(AlertThresholdOperator.IS_ABOVE);
expect(state.matchType).toBe(AlertThresholdMatchType.AT_LEAST_ONCE);
});
});
describe('getCreateAlertLocalStateFromAlertDef', () => {
it('should return the correct create alert local state for the given alert def', () => {
const args: PostableAlertRuleV2 = {

View File

@@ -10,11 +10,13 @@ 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';
@@ -215,17 +217,14 @@ export function CreateAlertProvider(
const {
mutate: createAlertRule,
isLoading: isCreatingAlertRule,
} = useCreateAlertRule();
} = useCreateRule();
const {
mutate: testAlertRule,
isLoading: isTestingAlertRule,
} = useTestAlertRule();
const { mutate: testAlertRule, isLoading: isTestingAlertRule } = useTestRule();
const {
mutate: updateAlertRule,
isLoading: isUpdatingAlertRule,
} = useUpdateAlertRule(ruleId || '');
} = useUpdateRuleByID();
const contextValue: ICreateAlertContextProps = useMemo(
() => ({
@@ -249,6 +248,7 @@ export function CreateAlertProvider(
updateAlertRule,
isUpdatingAlertRule,
isEditMode: isEditMode || false,
ruleId: ruleId || '',
}),
[
createAlertState,
@@ -267,6 +267,7 @@ export function CreateAlertProvider(
updateAlertRule,
isUpdatingAlertRule,
isEditMode,
ruleId,
],
);

View File

@@ -1,12 +1,15 @@
import { Dispatch } from 'react';
import { UseMutateFunction } from 'react-query';
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
import { UpdateAlertRuleResponse } from 'api/alerts/updateAlertRule';
import type {
CreateRule201,
RenderErrorResponseDTO,
RuletypesPostableRuleDTO,
TestRule200,
UpdateRuleByIDPathParameters,
} from 'api/generated/services/sigNoz.schemas';
import type { BodyType, ErrorType } from 'api/generatedAPIInstance';
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 {
@@ -24,27 +27,33 @@ export interface ICreateAlertContextProps {
setNotificationSettings: Dispatch<NotificationSettingsAction>;
isCreatingAlertRule: boolean;
createAlertRule: UseMutateFunction<
SuccessResponse<CreateAlertRuleResponse, unknown> | ErrorResponse,
Error,
PostableAlertRuleV2,
CreateRule201,
ErrorType<unknown>,
{ data: BodyType<RuletypesPostableRuleDTO> },
unknown
>;
isTestingAlertRule: boolean;
testAlertRule: UseMutateFunction<
SuccessResponse<TestAlertRuleResponse, unknown> | ErrorResponse,
Error,
PostableAlertRuleV2,
TestRule200,
ErrorType<unknown>,
{ data: BodyType<RuletypesPostableRuleDTO> },
unknown
>;
discardAlertRule: () => void;
isUpdatingAlertRule: boolean;
updateAlertRule: UseMutateFunction<
SuccessResponse<UpdateAlertRuleResponse, unknown> | ErrorResponse,
Error,
PostableAlertRuleV2,
Awaited<
ReturnType<typeof import('api/generated/services/rules').updateRuleByID>
>,
ErrorType<RenderErrorResponseDTO>,
{
pathParams: UpdateRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
unknown
>;
isEditMode: boolean;
ruleId: string;
}
export interface ICreateAlertProviderProps {
@@ -86,25 +95,28 @@ export interface Threshold {
}
export enum AlertThresholdOperator {
IS_ABOVE = '1',
IS_BELOW = '2',
IS_EQUAL_TO = '3',
IS_NOT_EQUAL_TO = '4',
ABOVE_BELOW = '7',
IS_ABOVE = 'above',
IS_BELOW = 'below',
IS_EQUAL_TO = 'equal',
IS_NOT_EQUAL_TO = 'not_equal',
ABOVE_BELOW = 'outside_bounds',
}
export enum AlertThresholdMatchType {
AT_LEAST_ONCE = '1',
ALL_THE_TIME = '2',
ON_AVERAGE = '3',
IN_TOTAL = '4',
LAST = '5',
AT_LEAST_ONCE = 'at_least_once',
ALL_THE_TIME = 'all_the_times',
ON_AVERAGE = 'on_average',
IN_TOTAL = 'in_total',
LAST = 'last',
}
export interface AlertThresholdState {
selectedQuery: string;
operator: AlertThresholdOperator;
matchType: AlertThresholdMatchType;
// Stored as a raw string so backend aliases ("1", ">", "above_or_eq", ...)
// survive a load/save round-trip. User edits from the UI write the
// canonical enum value.
operator: AlertThresholdOperator | string;
matchType: AlertThresholdMatchType | string;
evaluationWindow: string;
algorithm: string;
seasonality: string;

View File

@@ -237,6 +237,68 @@ export function getAdvancedOptionsStateFromAlertDef(
};
}
// Mirrors the backend's CompareOperator.Normalize() in
// pkg/types/ruletypes/compare.go. Maps any accepted alias to the enum value
// the dropdown understands. Returns undefined for aliases the UI does not
// expose (e.g. above_or_equal, below_or_equal) so callers can keep the raw
// value on screen instead of silently rewriting it.
export function normalizeOperator(
raw: string | undefined,
): AlertThresholdOperator | undefined {
switch (raw) {
case '1':
case 'above':
case '>':
return AlertThresholdOperator.IS_ABOVE;
case '2':
case 'below':
case '<':
return AlertThresholdOperator.IS_BELOW;
case '3':
case 'equal':
case 'eq':
case '=':
return AlertThresholdOperator.IS_EQUAL_TO;
case '4':
case 'not_equal':
case 'not_eq':
case '!=':
return AlertThresholdOperator.IS_NOT_EQUAL_TO;
case '7':
case 'outside_bounds':
return AlertThresholdOperator.ABOVE_BELOW;
default:
return undefined;
}
}
// Mirrors the backend's MatchType.Normalize() in pkg/types/ruletypes/match.go.
export function normalizeMatchType(
raw: string | undefined,
): AlertThresholdMatchType | undefined {
switch (raw) {
case '1':
case 'at_least_once':
return AlertThresholdMatchType.AT_LEAST_ONCE;
case '2':
case 'all_the_times':
return AlertThresholdMatchType.ALL_THE_TIME;
case '3':
case 'on_average':
case 'avg':
return AlertThresholdMatchType.ON_AVERAGE;
case '4':
case 'in_total':
case 'sum':
return AlertThresholdMatchType.IN_TOTAL;
case '5':
case 'last':
return AlertThresholdMatchType.LAST;
default:
return undefined;
}
}
export function getThresholdStateFromAlertDef(
alertDef: PostableAlertRuleV2,
): AlertThresholdState {
@@ -254,11 +316,9 @@ export function getThresholdStateFromAlertDef(
})) || [],
selectedQuery: alertDef.condition.selectedQueryName || '',
operator:
(alertDef.condition.thresholds?.spec[0].op as AlertThresholdOperator) ||
AlertThresholdOperator.IS_ABOVE,
alertDef.condition.thresholds?.spec[0].op || AlertThresholdOperator.IS_ABOVE,
matchType:
(alertDef.condition.thresholds?.spec[0]
.matchType as AlertThresholdMatchType) ||
alertDef.condition.thresholds?.spec[0].matchType ||
AlertThresholdMatchType.AT_LEAST_ONCE,
};
}

View File

@@ -48,7 +48,6 @@ 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,8 +16,6 @@ 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';
@@ -215,7 +213,7 @@ function PublicDashboardSetting(): JSX.Element {
try {
setCopyPublicDashboardURL(
getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
`${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
);
toast.success('Copied Public Dashboard URL successfully');
} catch (error) {
@@ -224,7 +222,7 @@ function PublicDashboardSetting(): JSX.Element {
};
const publicDashboardURL = useMemo(
() => getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
() => `${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
[publicDashboardResponse],
);
@@ -296,7 +294,7 @@ function PublicDashboardSetting(): JSX.Element {
icon={<ExternalLink size={12} />}
onClick={(): void => {
if (publicDashboardURL) {
openInNewTab(publicDashboardURL);
window.open(publicDashboardURL, '_blank');
}
}}
/>

View File

@@ -1,6 +1,5 @@
import { useCallback, useRef } from 'react';
import { Button } from 'antd';
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import ROUTES from 'constants/routes';
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -27,7 +26,7 @@ function DashboardBreadcrumbs(): JSX.Element {
const { title = '', image = Base64Icons[0] } = selectedData || {};
const goToListPage = useCallback(() => {
const dashboardsListQueryParamsString = getSessionStorageApi(
const dashboardsListQueryParamsString = sessionStorage.getItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
);

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import removeLocalStorageKey from 'api/browser/localstorage/remove';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { GraphVisibilityState, SeriesVisibilityItem } from '../types';
@@ -15,7 +12,7 @@ export function getStoredSeriesVisibility(
widgetId: string,
): SeriesVisibilityItem[] | null {
try {
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
if (!storedData) {
return null;
@@ -32,7 +29,7 @@ export function getStoredSeriesVisibility(
} catch (error) {
if (error instanceof SyntaxError) {
// If the stored data is malformed, remove it
removeLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
localStorage.removeItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
}
// Silently handle parsing errors - fall back to default visibility
return null;
@@ -45,7 +42,7 @@ export function updateSeriesVisibilityToLocalStorage(
): void {
let visibilityStates: GraphVisibilityState[] = [];
try {
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
visibilityStates = JSON.parse(storedData || '[]');
} catch (error) {
if (error instanceof SyntaxError) {
@@ -66,7 +63,7 @@ export function updateSeriesVisibilityToLocalStorage(
];
}
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
JSON.stringify(visibilityStates),
);

View File

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

View File

@@ -22,8 +22,6 @@ import {
Tooltip,
Typography,
} from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import { TelemetryFieldKey } from 'api/v5/v5';
import axios from 'axios';
@@ -474,7 +472,7 @@ function ExplorerOptions({
value: string;
}): void => {
// Retrieve stored views from local storage
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
// Initialize or parse the stored views
const updatedViews: PreservedViewsInLocalStorage = storedViews
@@ -488,7 +486,7 @@ function ExplorerOptions({
};
// Save the updated views back to local storage
setLocalStorageKey(
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(updatedViews),
);
@@ -539,7 +537,7 @@ function ExplorerOptions({
const removeCurrentViewFromLocalStorage = (): void => {
// Retrieve stored views from local storage
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
if (storedViews) {
// Parse the stored views
@@ -549,7 +547,7 @@ function ExplorerOptions({
delete parsedViews[PRESERVED_VIEW_TYPE];
// Update local storage with the modified views
setLocalStorageKey(
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(parsedViews),
);
@@ -674,7 +672,7 @@ function ExplorerOptions({
}
const parsedPreservedView = JSON.parse(
getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
);
const preservedView = parsedPreservedView[PRESERVED_VIEW_TYPE] || {};

View File

@@ -1,6 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
@@ -73,7 +71,7 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
try {
const showExplorerToolbar = getLocalStorageKey(
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar === null) {
@@ -86,7 +84,7 @@ export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
[DataSource.TRACES]: true,
[DataSource.LOGS]: true,
};
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
@@ -105,13 +103,13 @@ export const setExplorerToolBarVisibility = (
dataSource: string,
): void => {
try {
const showExplorerToolbar = getLocalStorageKey(
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar) {
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
parsedShowExplorerToolbar[dataSource] = value;
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);

View File

@@ -10,7 +10,6 @@ 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';
@@ -106,7 +105,7 @@ function ForgotPassword({
data: {
email: values.email,
orgId: currentOrgId,
frontendBaseURL: getBaseUrl(),
frontendBaseURL: window.location.origin,
},
});
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);

View File

@@ -15,7 +15,6 @@ 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';
@@ -88,7 +87,7 @@ function BasicInfo({
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
ruleId: isNewRule ? 0 : alertDef?.id,
});
openInNewTab(ROUTES.CHANNELS_NEW);
window.open(ROUTES.CHANNELS_NEW, '_blank');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasLoggedEvent = useRef(false);

View File

@@ -6,9 +6,15 @@ 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';
@@ -32,13 +38,16 @@ 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';
@@ -46,7 +55,6 @@ 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';
@@ -369,6 +377,7 @@ function FormAlertRules({
redirectWithQueryBuilderData(query);
};
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const validatePromParams = useCallback((): boolean => {
let retval = true;
@@ -534,59 +543,47 @@ function FormAlertRules({
};
try {
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);
if (ruleId && !isEmpty(ruleId)) {
await updateRuleByID(
{ id: ruleId },
toPostableRuleDTOFromAlertDef(postableAlert),
);
} else {
logData = {
status: 'error',
statusMessage: response.error || t('unexpected_error'),
};
notifications.error({
message: 'Error',
description: logData.statusMessage,
});
await createRule(toPostableRuleDTOFromAlertDef(postableAlert));
}
} catch (e) {
logData = {
status: 'error',
statusMessage: t('unexpected_error'),
status: 'success',
statusMessage: isNewRule ? t('rule_created') : t('rule_edited'),
};
notifications.error({
message: 'Error',
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'),
};
showErrorModal(apiError as APIError);
}
setLoading(false);
@@ -642,39 +639,30 @@ function FormAlertRules({
let statusResponse = { status: 'failed', message: '' };
setLoading(true);
try {
const response = await testAlertApi({ data: postableAlert });
const response = await testRule(
toPostableRuleDTOFromAlertDef(postableAlert),
);
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 {
if (response.data?.alertCount === 0) {
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
description: t('no_alerts_found'),
});
statusResponse = {
status: 'failed',
message: 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') };
}
} catch (e) {
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
statusResponse = { status: 'failed', message: t('unexpected_error') };
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
statusResponse = {
status: 'failed',
message: apiError?.getErrorMessage() || t('unexpected_error'),
};
showErrorModal(apiError as APIError);
}
setLoading(false);
logEvent('Alert: Test notification', {
@@ -772,7 +760,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
link: url,
});
openInNewTab(url);
window.open(url, '_blank');
}
}

View File

@@ -1,5 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import getLabelName from 'lib/getLabelName';
import { QueryData } from 'types/api/widgets/getQuery';
@@ -102,7 +100,7 @@ export const saveLegendEntriesToLocalStorage = ({
try {
existingEntries = JSON.parse(
getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
);
} catch (error) {
console.error('Error parsing LEGEND_GRAPH from local storage', error);
@@ -117,7 +115,7 @@ export const saveLegendEntriesToLocalStorage = ({
}
try {
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
JSON.stringify(existingEntries),
);

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import type { NotificationInstance } from 'antd/es/notification/interface';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { NavigateToExplorerProps } from 'components/CeleryTask/useNavigateToExplorer';
import { LOCALSTORAGE } from 'constants/localStorage';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -45,8 +44,8 @@ export const getLocalStorageGraphVisibilityState = ({
],
};
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = getLocalStorageKey(
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = localStorage.getItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
);
let legendFromLocalStore: {
@@ -95,8 +94,8 @@ export const getGraphVisibilityStateOnDataChange = ({
graphVisibilityStates: Array(options.series.length).fill(true),
legendEntry: showAllDataSet(options),
};
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = getLocalStorageKey(
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = localStorage.getItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
);
let legendFromLocalStore: {

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 { GettableAlert } from 'types/api/alerts/get';
import { toCompositeMetricQuery } from 'types/api/alerts/convert';
import { USER_ROLES } from 'types/roles';
import beaconUrl from '@/assets/Icons/beacon.svg';
@@ -28,22 +28,23 @@ export default function AlertRules({
const { user } = useAppContext();
const [rulesExist, setRulesExist] = useState(false);
const [sortedAlertRules, setSortedAlertRules] = useState<GettableAlert[]>([]);
const [sortedAlertRules, setSortedAlertRules] = useState<RuletypesRuleDTO[]>(
[],
);
const location = useLocation();
const params = new URLSearchParams(location.search);
// Fetch Alerts
const { data: alerts, isError, isLoading } = useQuery('allAlerts', {
queryFn: getAll,
cacheTime: 0,
const { data: alerts, isError, isLoading } = useListRules({
query: { cacheTime: 0 },
});
useEffect(() => {
const rules = alerts?.payload || [];
const rules = alerts?.data ?? [];
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;
@@ -52,10 +53,10 @@ export default function AlertRules({
return 1;
}
// Then sort by updateAt timestamp
const aUpdateAt = new Date(a.updateAt).getTime();
const bUpdateAt = new Date(b.updateAt).getTime();
return bUpdateAt - aUpdateAt;
// Then sort by updatedAt timestamp
return (
new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime()
);
});
if (sortedRules.length > 0 && !loadingUserPreferences) {
@@ -118,22 +119,27 @@ export default function AlertRules({
</div>
);
const onEditHandler = (record: GettableAlert) => (): void => {
const onEditHandler = (record: RuletypesRuleDTO) => (): void => {
logEvent('Homepage: Alert clicked', {
ruleId: record.id,
ruleName: record.alert,
ruleState: record.state,
});
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
const compositeQuery = mapQueryDataFromApi(
toCompositeMetricQuery(record.condition.compositeQuery),
);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType);
const panelType = record.condition.compositeQuery.panelType;
if (panelType) {
params.set(QueryParams.panelTypes, panelType);
}
params.set(QueryParams.ruleId, record.id.toString());
params.set(QueryParams.ruleId, record.id);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
};
@@ -169,9 +175,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,7 +10,6 @@ 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';
@@ -115,7 +114,7 @@ export default function Dashboards({
dashboardName: dashboard.data.title,
});
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}

View File

@@ -9,7 +9,6 @@ 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';
@@ -52,7 +51,7 @@ function DataSourceInfo({
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
}
};

View File

@@ -8,7 +8,6 @@ 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';
@@ -100,7 +99,11 @@ function HomeChecklist({
) {
history.push(item.toRoute || '');
} else {
openInNewTab(item.docsLink || '');
window?.open(
item.docsLink || '',
'_blank',
'noopener noreferrer',
);
}
}}
>
@@ -116,7 +119,7 @@ function HomeChecklist({
step: item.id,
});
openInNewTab(item.docsLink ?? '');
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
}}
>
<BookOpenText size={16} />

View File

@@ -31,7 +31,6 @@ 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';
@@ -80,7 +79,11 @@ const EmptyState = memo(
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
>

View File

@@ -17,7 +17,6 @@ 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';
@@ -134,7 +133,11 @@ export default function ServiceTraces({
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
>

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { ArrowRightOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { openInNewTab } from 'utils/navigation';
interface AlertInfoCardProps {
header: string;
@@ -20,7 +19,7 @@ function AlertInfoCard({
className="alert-info-card"
onClick={(): void => {
onClick();
openInNewTab(link);
window.open(link, '_blank');
}}
>
<div className="alert-card-text">

View File

@@ -1,6 +1,5 @@
import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons';
import { Flex, Typography } from 'antd';
import { openInNewTab } from 'utils/navigation';
interface InfoLinkTextProps {
infoText: string;
@@ -21,7 +20,7 @@ function InfoLinkText({
<Flex
onClick={(): void => {
onClick();
openInNewTab(link);
window.open(link, '_blank');
}}
className="info-link-container"
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,78 +1,66 @@
import { useEffect, useRef } from 'react';
import { useEffect, useMemo, 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, status } = useQuery('allAlerts', {
queryFn: getAll,
cacheTime: 0,
const { data, isError, isLoading, refetch, error } = useListRules({
query: { 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 && !isUndefined(data?.payload)) {
if (!logEventCalledRef.current && hasLoaded) {
logEvent('Alert: List page visited', {
number: data?.payload?.length,
number: rules.length,
});
logEventCalledRef.current = true;
}
}, [data?.payload]);
}, [hasLoaded, rules.length]);
useEffect(() => {
if (status === 'error' || (status === 'success' && data.statusCode >= 400)) {
if (isError) {
notifications.error({
message: data?.error || t('something_went_wrong'),
message: apiError?.getErrorMessage() || t('something_went_wrong'),
});
}
}, [data?.error, data?.statusCode, status, t, notifications]);
}, [isError, apiError, t, notifications]);
// api failed to load the data
if (isError) {
return <div>{data?.error || t('something_went_wrong')}</div>;
return <div>{apiError?.getErrorMessage() || t('something_went_wrong')}</div>;
}
// api is successful but error is present
if (status === 'success' && data.statusCode >= 400) {
return (
<ListAlert
{...{
allAlertRules: [],
refetch,
}}
/>
);
}
if (status === 'success' && !data.payload?.length) {
return <AlertsEmptyState />;
}
// in case of loading
if (isLoading || !data?.payload) {
if (isLoading || !data) {
return <Spinner height="75vh" tip="Loading Rules..." />;
}
if (rules.length === 0) {
return <AlertsEmptyState />;
}
return (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<ListAlert
{...{
allAlertRules: data.payload,
refetch,
}}
/>
<ListAlert allAlertRules={rules} refetch={refetch} />
</Space>
);
}

View File

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

View File

@@ -28,8 +28,6 @@ import {
Typography,
} from 'antd';
import type { TableProps } from 'antd/lib';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/v1/dashboards/create';
import { AxiosError } from 'axios';
@@ -85,8 +83,6 @@ 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';
@@ -149,7 +145,7 @@ function DashboardsList(): JSX.Element {
);
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
const dashboardDynamicColumnsString = getLocalStorageKey('dashboard');
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
let dashboardDynamicColumns: DashboardDynamicColumns = {
createdAt: true,
createdBy: true,
@@ -163,7 +159,7 @@ function DashboardsList(): JSX.Element {
);
if (isEmpty(tempDashboardDynamicColumns)) {
setLocalStorageKey('dashboard', JSON.stringify(dashboardDynamicColumns));
localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns));
} else {
dashboardDynamicColumns = { ...tempDashboardDynamicColumns };
}
@@ -171,7 +167,7 @@ function DashboardsList(): JSX.Element {
console.error(error);
}
} else {
setLocalStorageKey('dashboard', JSON.stringify(dashboardDynamicColumns));
localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns));
}
return dashboardDynamicColumns;
@@ -185,7 +181,7 @@ function DashboardsList(): JSX.Element {
visibleColumns: DashboardDynamicColumns,
): void {
try {
setLocalStorageKey('dashboard', JSON.stringify(visibleColumns));
localStorage.setItem('dashboard', JSON.stringify(visibleColumns));
} catch (error) {
console.error(error);
}
@@ -461,7 +457,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(getLink());
window.open(getLink(), '_blank');
}}
>
Open in New Tab
@@ -473,7 +469,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(getLink()));
setCopy(`${window.location.origin}${getLink()}`);
}}
>
Copy Link

View File

@@ -1,7 +1,6 @@
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';
@@ -13,7 +12,7 @@ function Name(name: Data['name'], data: Data): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
window.open(getLink(), '_blank');
} else {
history.push(getLink());
}

View File

@@ -17,7 +17,6 @@ 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';
@@ -117,7 +116,7 @@ function ContextLogRenderer({
);
const link = `${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`;
window.open(withBasePath(link), '_blank', 'noopener,noreferrer');
window.open(link, '_blank', 'noopener,noreferrer');
},
[query, urlQuery],
);

View File

@@ -34,7 +34,6 @@ 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';
@@ -192,7 +191,7 @@ function TableView({
if (event.ctrlKey || event.metaKey) {
// open the trace in new tab
openInNewTab(route);
window.open(route, '_blank');
} else {
history.push(route);
}

View File

@@ -213,7 +213,6 @@ 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,7 +34,6 @@ 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';
@@ -240,7 +239,7 @@ const TanStackTableView = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
urlQuery.delete(QueryParams.activeLogId);
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);
toast.success('Copied to clipboard', { position: 'top-right' });

View File

@@ -1,6 +1,5 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { withBasePath } from 'utils/basePath';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
@@ -38,7 +37,7 @@ export const navigateToTrace = ({
}=${JSONCompositeQuery}`;
if (openInNewTab) {
window.open(withBasePath(newTraceExplorerPath), '_blank');
window.open(newTraceExplorerPath, '_blank');
} else {
safeNavigate(newTraceExplorerPath);
}

View File

@@ -9,7 +9,6 @@ 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';
@@ -68,8 +67,9 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={alert.alertId}
onClick={(): void => {
openInNewTab(
window.open(
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
'_blank',
);
}}
className="dashboards-popover-content-item"
@@ -90,10 +90,11 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={dashboard.dashboardId}
onClick={(): void => {
openInNewTab(
window.open(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboardId,
}),
'_blank',
);
}}
className="dashboards-popover-content-item"

View File

@@ -18,7 +18,6 @@ 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';
@@ -85,7 +84,7 @@ function UpdateContextLinks({
);
// Function to get current domain
const getCurrentDomain = (): string => getBaseUrl();
const getCurrentDomain = (): string => window.location.origin;
// Function to handle variable selection from dropdown
const handleVariableSelect = (

View File

@@ -6,7 +6,6 @@ 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';
@@ -43,11 +42,11 @@ export default function NoLogs({
}
history.push(link);
} else if (dataSource === 'traces') {
openInNewTab(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE);
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
} else if (dataSource === DataSource.METRICS) {
openInNewTab(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE);
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
} else {
openInNewTab(`${DOCLINKS.USER_GUIDE}${dataSource}/`);
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
}
};
return (

View File

@@ -18,7 +18,6 @@ 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';
@@ -61,7 +60,7 @@ function InviteTeamMembers({
email: '',
role: '',
name: '',
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
id: '',
};

View File

@@ -8,7 +8,6 @@ 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';
@@ -216,7 +215,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
</a>
. To create a new one,{' '}
<a
href={withBasePath('/settings/ingestion-settings')}
href="/settings/ingestion-settings"
target="_blank"
className="learn-more"
rel="noreferrer"

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