Compare commits

..

104 Commits

Author SHA1 Message Date
Piyush Singariya
e39e93b01a Merge branch 'main' into feat/json-qb-tests 2026-04-22 14:52:29 +05:30
Piyush Singariya
8fbc490673 ci: polluted test fix 2026-04-22 14:52:16 +05:30
Pandey
5b599babd5 refactor(ruler): migrate frontend to generated API clients for rules and downtime schedules (#10968)
* 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.

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

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

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

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

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

* 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

* 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

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

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

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

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

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

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

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

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

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

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

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

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

* 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).

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

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

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

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

* refactor(frontend): drop optional chains on fields now required by generated types

* refactor(frontend): drop Record cast in PlannedDowntime recurrence spread

* refactor(frontend): type formData seed as Partial in PlannedDowntimeForm

* refactor(frontend): type defautlInitialValues as Partial directly

* refactor(frontend): drop as unknown from copyAlert cast in clone flow

* refactor(frontend): add dataSourceForAlertType helper keyed on generated enum

* refactor(frontend): align defautlInitialValues createdBy with createdAt as undefined

* refactor(frontend): surface BE error via convertToApiError on save alert paths

* refactor(frontend): adopt renamed RuletypesRuleDTO and created/updated field names

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

* 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().

* refactor(frontend): add typed converters between local alert types and generated DTOs

* refactor(frontend): add patchRulePartial wrapper for Partial rule patches

* refactor(frontend): replace inline alert DTO casts with typed converters

* 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).

* chore: parity with expected enum values of match type and compare operator

* chore: remove unused Typography import

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-04-22 09:07:47 +00:00
Piyush Singariya
88c53c456d fix: tests 2026-04-22 13:42:22 +05:30
Piyush Singariya
8cda4644fb Merge branch 'main' into feat/json-qb-tests 2026-04-22 13:26:00 +05:30
Piyush Singariya
c3a73c6626 fix: index needs body prefix 2026-04-22 13:25:20 +05:30
Piyush Singariya
6be50ec7bc ci: fix go tests 2026-04-22 13:23:35 +05:30
Piyush Singariya
98cefa1975 fix: openapi ci 2026-04-22 13:23:23 +05:30
primus-bot[bot]
3080c3c493 chore(release): bump to v0.120.0 (#11052)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-04-22 07:11:54 +00:00
Vinicius Lourenço
b0e9fbe24f test(organization-traces): try unflaky both tests (#11046)
* test(organization-traces): try unflaky both tests

* fix: improve test speeds

* fix: increase timeout

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-04-22 07:11:25 +00:00
Piyush Singariya
43bbde1887 Merge branch 'main' into feat/json-qb-tests 2026-04-22 12:38:55 +05:30
Piyush Singariya
d91c870fb5 fix: change based on review 2026-04-22 12:38:12 +05:30
Abhi kumar
aeb71469d9 fix: minor tooltip css fix (#11053) 2026-04-22 07:04:29 +00:00
Piyush Singariya
5a441d3367 fix: change based on review 2026-04-22 12:07:13 +05:30
Ashwin Bhatkal
1ff9a748ee refactor: rename selectedDashboard to dashboardData (#11040)
Renames the `selectedDashboard` store field (and related setters/getters
`setSelectedDashboard`, `getSelectedDashboard`) to `dashboardData` across
the frontend. Also renames incidental locals (`updatedSelectedDashboard`,
`mockSelectedDashboard`, and the ExportPanel's local `selectedDashboardId`).
2026-04-22 05:58:21 +00:00
Piyush Singariya
ddf0975cf6 chore: comment removal 2026-04-21 17:41:40 +05:30
Piyush Singariya
3f8a5af14c test: enhanced tests to work with backtick required key 2026-04-21 16:10:59 +05:30
Piyush Singariya
65ce61c0dd Merge branch 'main' into feat/json-qb-tests 2026-04-21 15:33:59 +05:30
Piyush Singariya
1350dd8226 chore: some changes in test to improve quality 2026-04-21 15:33:45 +05:30
Piyush Singariya
14ecd0f8db chore: go mod tidy 2026-04-21 13:35:09 +05:30
Piyush Singariya
7eccbc96b6 chore: error fix and unused code 2026-04-21 13:33:18 +05:30
Piyush Singariya
0a595e1b71 chore: update migrator version to stable release 2026-04-21 13:30:12 +05:30
Piyush Singariya
e8ed7592e6 chore: update collector to stable release 2026-04-21 13:28:54 +05:30
Piyush Singariya
eee7788108 fix: replacing JSONIndex with TelemetryFieldKeyIndex 2026-04-21 13:25:06 +05:30
Piyush Singariya
0a37aa0bd1 Merge branch 'main' into feat/json-qb-tests 2026-04-21 10:59:26 +05:30
Piyush Singariya
7d7307a6eb Merge branch 'chore/json-changes' into feat/json-qb-tests 2026-04-16 11:50:14 +05:30
Piyush Singariya
91beeb4949 fix: int64 mapping 2026-04-16 11:49:40 +05:30
Piyush Singariya
9f8c3466b1 test: select orderby works 2026-04-16 11:49:15 +05:30
Piyush Singariya
701323c822 Merge branch 'chore/json-changes' into feat/json-qb-tests 2026-04-15 18:18:00 +05:30
Piyush Singariya
cc193c9be5 fix: issue with FuzzyMatching and API failing 2026-04-15 18:17:44 +05:30
Piyush Singariya
512dc579b1 ci: test with updated collector 2026-04-15 16:54:18 +05:30
Piyush Singariya
d904953afe Merge branch 'chore/json-changes' into feat/json-qb-tests 2026-04-15 16:08:58 +05:30
Piyush Singariya
9690c06cb2 fix: comment removed 2026-04-15 16:08:44 +05:30
Piyush Singariya
8c44925a67 fix: branches should tell missing array types 2026-04-15 16:08:14 +05:30
Piyush Singariya
1ea35a19d7 fix: update jsontypeexporter 2026-04-15 15:59:46 +05:30
Piyush Singariya
c27db82794 test: select order by tests added 2026-04-15 15:35:19 +05:30
Piyush Singariya
5d9685ad9e test: added indexed tests separately 2026-04-15 15:08:32 +05:30
Piyush Singariya
8d0cc2d99d Merge branch 'chore/json-changes' into feat/json-qb-tests 2026-04-15 14:52:49 +05:30
Piyush Singariya
9643fae27a fix: invalid index usage on terminal condition 2026-04-15 14:51:24 +05:30
Piyush Singariya
750394a73f Merge branch 'chore/json-changes' into feat/json-qb-tests 2026-04-15 14:18:53 +05:30
Piyush Singariya
19a65e80d8 Merge branch 'main' into chore/json-changes 2026-04-15 14:18:32 +05:30
Piyush Singariya
bbef04352e Merge branch 'chore/json-changes' into feat/json-qb-tests 2026-04-15 14:17:04 +05:30
Piyush Singariya
0758ff133b fix: json access plan remval of AvailableTypes 2026-04-15 14:16:23 +05:30
Piyush Singariya
74dceb844b test: integration test fix attempt 1 2026-04-15 13:54:30 +05:30
Piyush Singariya
0d0ebe8fe7 Merge branch 'main' into feat/json-qb-tests 2026-04-15 13:07:34 +05:30
Piyush Singariya
b35a6213a6 fix: remove unused promoted fixture 2026-04-15 13:05:52 +05:30
Piyush Singariya
9cef109158 fix: changes based on review 2026-04-15 13:00:14 +05:30
Piyush Singariya
bb308c263f chore: changes based on review 2026-04-15 11:30:29 +05:30
Piyush Singariya
67e1b82adb fix: name changed to field_map 2026-04-14 17:05:30 +05:30
Piyush Singariya
1e8c0f19f5 fix: added safeguards in plan generation 2026-04-14 16:17:44 +05:30
Piyush Singariya
c027181935 test: selectField tests added 2026-04-14 16:06:12 +05:30
Piyush Singariya
7122fb8b54 revert: remove db binaries 2026-04-14 14:56:46 +05:30
Piyush Singariya
67830c8a16 Merge branch 'main' into chore/json-changes 2026-04-14 14:38:53 +05:30
Piyush Singariya
096eee6435 Merge branch 'upgrade/collector@v0.144.3-rc.4' into chore/json-changes 2026-04-14 14:26:31 +05:30
Piyush Singariya
30f5f2f2f2 ci: test fix 2026-04-14 13:34:49 +05:30
Piyush Singariya
52adb84461 Merge branch 'upgrade/collector@v0.144.3-rc.4' into chore/json-changes 2026-04-14 13:20:29 +05:30
Piyush Singariya
63b7f15d0e fix: tests 2026-04-14 13:19:30 +05:30
Piyush Singariya
6d3c88ed21 chore: upgrade collector version v0.144.3-rc.4 2026-04-14 13:07:43 +05:30
Piyush Singariya
fc6a67aec1 Merge branch 'upgrade/collector@v0.144.3-rc.2' into chore/json-changes 2026-04-14 12:58:26 +05:30
Piyush Singariya
9f41499047 fix: go sum 2026-04-14 12:43:58 +05:30
Piyush Singariya
9cd554d293 fix: qbtoexpr tests 2026-04-14 12:43:07 +05:30
Piyush Singariya
415387edfc fix: upgraded collector version 2026-04-14 12:40:37 +05:30
Piyush Singariya
fa1bc3db9b ci: tests fixed 2026-04-14 11:49:35 +05:30
Piyush Singariya
6c6dad8a66 chore: remove redundent comparison 2026-04-13 17:18:16 +05:30
Piyush Singariya
7403f86773 fix: tons of changes 2026-04-13 13:44:57 +05:30
Piyush Singariya
9176ef0589 Merge branch 'main' into feat/json-qb-tests 2026-04-10 19:59:46 +05:30
Piyush Singariya
5c2a338189 test: updated validation 2026-04-10 19:59:29 +05:30
Piyush Singariya
704bab23cf Merge branch 'main' into feat/json-qb-tests 2026-04-09 18:04:25 +05:30
Piyush Singariya
371da26b3c fix: test 2026-04-09 18:03:57 +05:30
Piyush Singariya
97fbfbdc13 fix: type ambiguity 2026-04-09 13:58:13 +05:30
Piyush Singariya
4b112988ef Merge branch 'main' into feat/json-qb-tests 2026-04-09 12:19:26 +05:30
Piyush Singariya
a47ecf3907 test: with higher migrator version 2026-04-09 12:16:31 +05:30
Piyush Singariya
e4a78cf556 fix: dynamically change insert stmt for body_v2 availability 2026-04-08 22:00:05 +05:30
Piyush Singariya
b6adecc294 fix: better validations 2026-04-08 19:29:28 +05:30
Piyush Singariya
40333a5fee Merge branch 'main' into feat/json-qb-tests 2026-04-08 19:08:34 +05:30
Piyush Singariya
4af6a9abae Merge branch 'main' into feat/json-qb-tests 2026-04-07 21:49:21 +05:30
Piyush Singariya
55e892dad3 fix: body tests ready 2026-04-07 16:22:26 +05:30
Piyush Singariya
181116308f fix: logs.py 2026-04-07 15:56:21 +05:30
Piyush Singariya
eaa678910b fix: better tests 2026-04-07 14:49:52 +05:30
Piyush Singariya
e994caeb02 chore: import tests from older pr 2026-04-07 13:43:16 +05:30
Piyush Singariya
10840f8495 ci: lint changes 2026-04-07 12:31:22 +05:30
Piyush Singariya
1fcd3adfc8 Merge branch 'main' into fix/array-json 2026-04-07 12:29:35 +05:30
Piyush Singariya
3e14b26b00 fix: negative operator check 2026-04-07 12:26:44 +05:30
Piyush Singariya
b30bfa6371 fix: better review for test file 2026-04-02 12:53:44 +05:30
Piyush Singariya
e7f4a04b36 Merge branch 'main' into fix/array-json 2026-04-01 15:44:01 +05:30
Piyush Singariya
0687634da3 fix: stringified integer value input 2026-04-01 15:25:35 +05:30
Piyush Singariya
7e7732243e fix: dynamic array tests 2026-04-01 12:54:26 +05:30
Piyush Singariya
2f952e402f feat: change filtering of dynamic arrays 2026-04-01 12:09:32 +05:30
Piyush Singariya
a12febca4a fix: array json element comparison 2026-04-01 10:34:55 +05:30
Piyush Singariya
cb71c9c3f7 Merge branch 'main' into fix/array-json 2026-03-31 15:42:09 +05:30
Piyush Singariya
1cd4ce6509 Merge branch 'main' into fix/array-json 2026-03-31 14:55:36 +05:30
Piyush Singariya
9299c8ab18 fix: indexed unit tests 2026-03-30 15:47:47 +05:30
Piyush Singariya
24749de269 fix: comment 2026-03-30 15:16:28 +05:30
Piyush Singariya
39098ec3f4 fix: unit tests 2026-03-30 15:12:17 +05:30
Piyush Singariya
fe554f5c94 fix: remove not used paths from testdata 2026-03-30 14:24:48 +05:30
Piyush Singariya
8a60a041a6 fix: unit tests 2026-03-30 14:14:49 +05:30
Piyush Singariya
541f19c34a fix: array type filtering from dynamic arrays 2026-03-30 12:59:31 +05:30
Piyush Singariya
010db03d6e fix: indexed tests passing 2026-03-30 12:24:26 +05:30
Piyush Singariya
5408acbd8c fix: primitive conditions working 2026-03-30 12:01:35 +05:30
Piyush Singariya
0de6c85f81 feat: align negative operators to include other logs 2026-03-28 10:30:11 +05:30
Piyush Singariya
69ec24fa05 test: fix unit tests 2026-03-27 15:12:49 +05:30
Piyush Singariya
539d732b65 fix: contextual path index usage 2026-03-27 14:44:51 +05:30
Piyush Singariya
843d5fb199 Merge branch 'main' into feat/json-index 2026-03-27 14:17:52 +05:30
Piyush Singariya
fabdfb8cc1 feat: enable JSON Path index 2026-03-27 14:07:37 +05:30
140 changed files with 4027 additions and 1746 deletions

View File

@@ -52,6 +52,7 @@ jobs:
- ingestionkeys
- rootuser
- serviceaccount
- querier_json_body
sqlstore-provider:
- postgres
- sqlite
@@ -61,7 +62,7 @@ jobs:
- 25.5.6
- 25.12.5
schema-migrator-version:
- v0.142.0
- v0.144.3
postgres-version:
- 15
if: |

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.119.0
image: signoz/signoz:v0.120.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
@@ -213,7 +213,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.2
image: signoz/signoz-otel-collector:v0.144.3
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.2
image: signoz/signoz-otel-collector:v0.144.3
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.119.0
image: signoz/signoz:v0.120.0
ports:
- "8080:8080" # signoz port
volumes:
@@ -139,7 +139,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.2
image: signoz/signoz-otel-collector:v0.144.3
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.2
image: signoz/signoz-otel-collector:v0.144.3
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.119.0}
image: signoz/signoz:${VERSION:-v0.120.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -204,7 +204,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.3}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -229,7 +229,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.3}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.119.0}
image: signoz/signoz:${VERSION:-v0.120.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -132,7 +132,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.3}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -157,7 +157,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.3}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -2705,8 +2705,8 @@ components:
type: object
PromotetypesWrappedIndex:
properties:
column_type:
type: string
fieldDataType:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
granularity:
type: integer
type:

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

@@ -3469,10 +3469,7 @@ export interface PromotetypesPromotePathDTO {
}
export interface PromotetypesWrappedIndexDTO {
/**
* @type string
*/
column_type?: string;
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
/**
* @type integer
*/

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

@@ -79,7 +79,7 @@ export function useNavigateToExplorer(): (
);
const { getUpdatedQuery } = useUpdatedQuery();
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
const { notifications } = useNotifications();
return useCallback(
@@ -111,7 +111,7 @@ export function useNavigateToExplorer(): (
panelTypes: PANEL_TYPES.TIME_SERIES,
timePreferance: 'GLOBAL_TIME',
},
selectedDashboard,
dashboardData,
})
.then((query) => {
preparedQuery = query;
@@ -140,7 +140,7 @@ export function useNavigateToExplorer(): (
minTime,
maxTime,
getUpdatedQuery,
selectedDashboard,
dashboardData,
notifications,
],
);

View File

@@ -87,8 +87,8 @@ jest.mock('hooks/useDarkMode', () => ({
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { selectedDashboard: undefined } => ({
selectedDashboard: undefined,
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));

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

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

@@ -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 { Button, toast } from '@signozhq/ui';
import { Tooltip } 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

@@ -196,12 +196,12 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
useDashboardStore.setState({
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
dashboardData: (getDashboardById.data as unknown) as Dashboard,
layouts: [],
panelMap: {},
setPanelMap: jest.fn(),
setLayouts: jest.fn(),
setSelectedDashboard: jest.fn(),
setDashboardData: jest.fn(),
columnWidths: {},
});

View File

@@ -78,12 +78,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const {
selectedDashboard,
dashboardData,
panelMap,
setPanelMap,
layouts,
setLayouts,
setSelectedDashboard,
setDashboardData,
} = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
@@ -98,10 +98,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
const selectedData = selectedDashboard
const selectedData = dashboardData
? {
...selectedDashboard.data,
uuid: selectedDashboard.id,
...dashboardData.data,
uuid: dashboardData.id,
}
: ({} as DashboardData);
const { dashboardVariables } = useDashboardVariables();
@@ -133,8 +133,8 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
let isAuthor = false;
if (selectedDashboard && user && user.email) {
isAuthor = selectedDashboard?.createdBy === user?.email;
if (dashboardData && user && user.email) {
isAuthor = dashboardData?.createdBy === user?.email;
}
let permissions: ComponentTypes[] = ['add_panel'];
@@ -146,7 +146,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { notifications } = useNotifications();
const userRole: ROLES | null =
selectedDashboard?.createdBy === user?.email
dashboardData?.createdBy === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
@@ -155,9 +155,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const onEmptyWidgetHandler = useCallback(() => {
setIsPanelTypeSelectionModalOpen(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
dashboardId: dashboardData?.id,
dashboardName: dashboardData?.data.title,
numberOfPanels: dashboardData?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);
@@ -168,14 +168,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
};
const onNameChangeHandler = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const updatedDashboard: Props = {
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
title: updatedTitle,
},
};
@@ -186,7 +186,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
});
setIsRenameDashboardOpen(false);
if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
setDashboardData(updatedDashboard.data);
}
},
onError: () => {
@@ -203,10 +203,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
// the context value is sometimes not available during the initial render
// due to which the updatedTitle is set to some previous value
useEffect(() => {
if (selectedDashboard) {
setUpdatedTitle(selectedDashboard.data.title);
if (dashboardData) {
setUpdatedTitle(dashboardData.data.title);
}
}, [selectedDashboard]);
}, [dashboardData]);
useEffect(() => {
if (state.error) {
@@ -227,7 +227,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
}, [state.error, state.value, t, notifications]);
function handleAddRow(): void {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const id = uuid();
@@ -246,10 +246,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
}
const updatedDashboard: Props = {
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
layout: [
{
i: id,
@@ -265,7 +265,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
],
panelMap: { ...panelMap, [id]: newRowWidgetMap },
widgets: [
...(selectedDashboard.data.widgets || []),
...(dashboardData.data.widgets || []),
{
id,
title: sectionName,
@@ -282,7 +282,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
if (updatedDashboard.data.data.layout) {
setLayouts(sortLayout(updatedDashboard.data.data.layout));
}
setSelectedDashboard(updatedDashboard.data);
setDashboardData(updatedDashboard.data);
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
}
@@ -299,8 +299,8 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
error: errorPublicDashboardData,
isError: isErrorPublicDashboardData,
} = useGetPublicDashboardMeta(
selectedDashboard?.id || '',
!!selectedDashboard?.id && isPublicDashboardEnabled,
dashboardData?.id || '',
!!dashboardData?.id && isPublicDashboardEnabled,
);
useEffect(() => {
@@ -378,14 +378,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
<Tooltip
title={
selectedDashboard?.createdBy === 'integration' &&
dashboardData?.createdBy === 'integration' &&
'Dashboards created by integrations cannot be unlocked'
}
>
<Button
type="text"
icon={<LockKeyhole size={14} />}
disabled={selectedDashboard?.createdBy === 'integration'}
disabled={dashboardData?.createdBy === 'integration'}
onClick={handleLockDashboardToggle}
data-testid="lock-unlock-dashboard"
>
@@ -457,9 +457,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
</section>
<section className="delete-dashboard">
<DeleteButton
createdBy={selectedDashboard?.createdBy || ''}
name={selectedDashboard?.data.title || ''}
id={String(selectedDashboard?.id) || ''}
createdBy={dashboardData?.createdBy || ''}
name={dashboardData?.data.title || ''}
id={String(dashboardData?.id) || ''}
isLocked={isDashboardLocked}
routeToListPage
/>

View File

@@ -239,7 +239,7 @@ function VariableItem({
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
useEffect(() => {
@@ -248,7 +248,7 @@ function VariableItem({
} else if (dynamicVariablesSelectedValue?.name) {
const widgets = getWidgetsHavingDynamicVariableAttribute(
dynamicVariablesSelectedValue?.name,
(selectedDashboard?.data?.widgets?.filter(
(dashboardData?.data?.widgets?.filter(
(widget) => widget.panelTypes !== PANEL_GROUP_TYPES.ROW,
) || []) as Widgets[],
variableData.name,
@@ -257,7 +257,7 @@ function VariableItem({
}
}, [
dynamicVariablesSelectedValue?.name,
selectedDashboard,
dashboardData,
variableData.id,
variableData.name,
widgetsByDynamicVariableId,

View File

@@ -12,17 +12,17 @@ export function WidgetSelector({
selectedWidgets: string[];
setSelectedWidgets: (widgets: string[]) => void;
}): JSX.Element {
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
// Get layout IDs for cross-referencing
const layoutIds = new Set(
(selectedDashboard?.data?.layout || []).map((item) => item.i),
(dashboardData?.data?.layout || []).map((item) => item.i),
);
// Filter and deduplicate widgets by ID, keeping only those with layout entries
// and excluding row widgets since they are not panels that can have variables
const widgets = Object.values(
(selectedDashboard?.data?.widgets || []).reduce(
(dashboardData?.data?.widgets || []).reduce(
(acc: Record<string, WidgetRow | Widgets>, widget: WidgetRow | Widgets) => {
if (
widget.id &&

View File

@@ -87,7 +87,7 @@ function VariablesSettings({
const { t } = useTranslation(['dashboard']);
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
const { dashboardData, setDashboardData } = useDashboardStore();
const { dashboardVariables } = useDashboardVariables();
const { notifications } = useNotifications();
@@ -173,7 +173,7 @@ function VariablesSettings({
widgetIds?: string[],
applyToAll?: boolean,
): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
@@ -181,16 +181,16 @@ function VariablesSettings({
(currentRequestedId &&
updatedVariablesData[currentRequestedId || '']?.type === 'DYNAMIC' &&
addDynamicVariableToPanels(
selectedDashboard,
dashboardData,
updatedVariablesData[currentRequestedId || ''],
widgetIds,
applyToAll,
)) ||
selectedDashboard;
dashboardData;
updateMutation.mutateAsync(
{
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...newDashboard.data,
@@ -200,7 +200,7 @@ function VariablesSettings({
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
setDashboardData(updatedDashboard.data);
notifications.success({
message: t('variable_updated_successfully'),
});

View File

@@ -15,11 +15,11 @@ import './GeneralSettings.styles.scss';
const { Option } = Select;
function GeneralDashboardSettings(): JSX.Element {
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
const { dashboardData, setDashboardData } = useDashboardStore();
const updateDashboardMutation = useUpdateDashboard();
const selectedData = selectedDashboard?.data;
const selectedData = dashboardData?.data;
const { title = '', tags = [], description = '', image = Base64Icons[0] } =
selectedData || {};
@@ -37,15 +37,15 @@ function GeneralDashboardSettings(): JSX.Element {
const { t } = useTranslation('common');
const onSaveHandler = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
updateDashboardMutation.mutate(
{
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
description: updatedDescription,
tags: updatedTags,
title: updatedTitle,
@@ -55,7 +55,7 @@ function GeneralDashboardSettings(): JSX.Element {
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
setDashboardData(updatedDashboard.data);
}
},
onError: () => {},

View File

@@ -41,7 +41,7 @@ const DASHBOARD_VARIABLES_WARNING =
// Use wildcard pattern to match both relative and absolute URLs in MSW
const publicDashboardURL = `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`;
const mockSelectedDashboard = {
const mockDashboardData = {
id: MOCK_DASHBOARD_ID,
data: {
title: 'Test Dashboard',
@@ -70,7 +70,7 @@ beforeEach(() => {
// Mock useDashboardStore
mockUseDashboard.mockReturnValue(({
selectedDashboard: mockSelectedDashboard,
dashboardData: mockDashboardData,
} as unknown) as ReturnType<typeof useDashboardStore>);
// Mock useCopyToClipboard

View File

@@ -59,7 +59,7 @@ function PublicDashboardSetting(): JSX.Element {
const [defaultTimeRange, setDefaultTimeRange] = useState(DEFAULT_TIME_RANGE);
const [, setCopyPublicDashboardURL] = useCopyToClipboard();
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
@@ -84,8 +84,8 @@ function PublicDashboardSetting(): JSX.Element {
refetch: refetchPublicDashboard,
error: errorPublicDashboard,
} = useGetPublicDashboardMeta(
selectedDashboard?.id || '',
!!selectedDashboard?.id && isPublicDashboardEnabled,
dashboardData?.id || '',
!!dashboardData?.id && isPublicDashboardEnabled,
);
const isPublicDashboard = !!publicDashboardData?.publicPath;
@@ -154,36 +154,36 @@ function PublicDashboardSetting(): JSX.Element {
});
const handleCreatePublicDashboard = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
createPublicDashboard({
dashboardId: selectedDashboard.id,
dashboardId: dashboardData.id,
timeRangeEnabled,
defaultTimeRange,
});
};
const handleUpdatePublicDashboard = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
updatePublicDashboard({
dashboardId: selectedDashboard.id,
dashboardId: dashboardData.id,
timeRangeEnabled,
defaultTimeRange,
});
};
const handleRevokePublicDashboardAccess = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
revokePublicDashboardAccess({
id: selectedDashboard.id,
id: dashboardData.id,
});
};

View File

@@ -26,10 +26,10 @@ import VariableItem from './VariableItem';
import './DashboardVariableSelection.styles.scss';
function DashboardVariableSelection(): JSX.Element | null {
const { dashboardId, setSelectedDashboard } = useDashboardStore(
const { dashboardId, setDashboardData } = useDashboardStore(
useShallow((s) => ({
dashboardId: s.selectedDashboard?.id ?? '',
setSelectedDashboard: s.setSelectedDashboard,
dashboardId: s.dashboardData?.id ?? '',
setDashboardData: s.setDashboardData,
})),
);
@@ -99,7 +99,7 @@ function DashboardVariableSelection(): JSX.Element | null {
// Synchronously update the external store with the new variable value so that
// child variables see the updated parent value when they refetch, rather than
// waiting for setSelectedDashboard → useEffect → updateDashboardVariablesStore.
// waiting for setDashboardData → useEffect → updateDashboardVariablesStore.
const updatedVariables = { ...dashboardVariables };
if (updatedVariables[id]) {
updatedVariables[id] = {
@@ -119,7 +119,7 @@ function DashboardVariableSelection(): JSX.Element | null {
}
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
setSelectedDashboard((prev) => {
setDashboardData((prev) => {
if (prev) {
const oldVariables = { ...prev?.data.variables };
// this is added to handle case where we have two different
@@ -157,7 +157,7 @@ function DashboardVariableSelection(): JSX.Element | null {
// Safe to call synchronously now that the store already has the updated value.
enqueueDescendantsOfVariable(name);
},
[dashboardId, dashboardVariables, updateUrlVariable, setSelectedDashboard],
[dashboardId, dashboardVariables, updateUrlVariable, setDashboardData],
);
return (

View File

@@ -30,11 +30,11 @@ const mockVariableItemCallbacks: {
} = {};
// Mock providers/Dashboard/Dashboard
const mockSetSelectedDashboard = jest.fn();
const mockSetDashboardData = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
interface MockDashboardStoreState {
selectedDashboard?: { id: string };
setSelectedDashboard: typeof mockSetSelectedDashboard;
dashboardData?: { id: string };
setDashboardData: typeof mockSetDashboardData;
updateLocalStorageDashboardVariables: typeof mockUpdateLocalStorageDashboardVariables;
}
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
@@ -42,8 +42,8 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
selector?: (s: Record<string, unknown>) => MockDashboardStoreState,
): MockDashboardStoreState => {
const state = {
selectedDashboard: { id: 'dash-1' },
setSelectedDashboard: mockSetSelectedDashboard,
dashboardData: { id: 'dash-1' },
setDashboardData: mockSetDashboardData,
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
};
return selector ? selector(state) : state;

View File

@@ -38,15 +38,11 @@ interface UseDashboardVariableUpdateReturn {
}
export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn => {
const {
dashboardId,
selectedDashboard,
setSelectedDashboard,
} = useDashboardStore(
const { dashboardId, dashboardData, setDashboardData } = useDashboardStore(
useShallow((s) => ({
dashboardId: s.selectedDashboard?.id ?? '',
selectedDashboard: s.selectedDashboard,
setSelectedDashboard: s.setSelectedDashboard,
dashboardId: s.dashboardData?.id ?? '',
dashboardData: s.dashboardData,
setDashboardData: s.setDashboardData,
})),
);
const addDynamicVariableToPanels = useAddDynamicVariableToPanels();
@@ -74,8 +70,8 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
isDynamic,
);
if (selectedDashboard) {
setSelectedDashboard((prev) => {
if (dashboardData) {
setDashboardData((prev) => {
if (prev) {
const oldVariables = prev?.data.variables;
// this is added to handle case where we have two different
@@ -110,7 +106,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
}
}
},
[dashboardId, selectedDashboard, setSelectedDashboard],
[dashboardId, dashboardData, setDashboardData],
);
const updateVariables = useCallback(
@@ -120,23 +116,23 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
widgetIds?: string[],
applyToAll?: boolean,
): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const newDashboard =
(currentRequestedId &&
addDynamicVariableToPanels(
selectedDashboard,
dashboardData,
updatedVariablesData[currentRequestedId || ''],
widgetIds,
applyToAll,
)) ||
selectedDashboard;
dashboardData;
updateMutation.mutateAsync(
{
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...newDashboard.data,
@@ -146,7 +142,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
setDashboardData(updatedDashboard.data);
// notifications.success({
// message: t('variable_updated_successfully'),
// });
@@ -155,12 +151,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
},
);
},
[
selectedDashboard,
addDynamicVariableToPanels,
updateMutation,
setSelectedDashboard,
],
[dashboardData, addDynamicVariableToPanels, updateMutation, setDashboardData],
);
const createVariable = useCallback(
@@ -172,13 +163,13 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
source: 'logs' | 'traces' | 'metrics' | 'all sources' = 'all sources',
// widgetId?: string,
): void => {
if (!selectedDashboard) {
if (!dashboardData) {
console.warn('No dashboard selected for variable creation');
return;
}
// Get current dashboard variables
const currentVariables = selectedDashboard.data.variables || {};
const currentVariables = dashboardData.data.variables || {};
// Create tableRowData like Dashboard Settings does
const tableRowData = [];
@@ -234,7 +225,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
const updatedVariables = convertVariablesToDbFormat(tableRowData);
updateVariables(updatedVariables, newVariable.id, [], false);
},
[selectedDashboard, updateVariables],
[dashboardData, updateVariables],
);
return {

View File

@@ -47,12 +47,12 @@ const mockDashboard = {
};
// Mock the dashboard provider with stable functions to prevent infinite loops
const mockSetSelectedDashboard = jest.fn();
const mockSetDashboardData = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: mockDashboard,
setSelectedDashboard: mockSetSelectedDashboard,
dashboardData: mockDashboard,
setDashboardData: mockSetDashboardData,
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
}),
}));

View File

@@ -58,7 +58,7 @@ const mockDashboard = {
// Mock dependencies
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: mockDashboard,
dashboardData: mockDashboard,
}),
}));
@@ -154,7 +154,7 @@ describe('Panel Management Tests', () => {
// Temporarily mock the dashboard
jest.doMock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: modifiedDashboard,
dashboardData: modifiedDashboard,
}),
}));

View File

@@ -13,13 +13,13 @@ import './DashboardBreadcrumbs.styles.scss';
function DashboardBreadcrumbs(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedDashboard } = useDashboardStore();
const updatedAtRef = useRef(selectedDashboard?.updatedAt);
const { dashboardData } = useDashboardStore();
const updatedAtRef = useRef(dashboardData?.updatedAt);
const selectedData = selectedDashboard
const selectedData = dashboardData
? {
...selectedDashboard.data,
uuid: selectedDashboard.id,
...dashboardData.data,
uuid: dashboardData.id,
}
: ({} as DashboardData);
@@ -31,7 +31,7 @@ function DashboardBreadcrumbs(): JSX.Element {
);
const hasDashboardBeenUpdated =
selectedDashboard?.updatedAt !== updatedAtRef.current;
dashboardData?.updatedAt !== updatedAtRef.current;
if (!hasDashboardBeenUpdated && dashboardsListQueryParamsString) {
safeNavigate({
pathname: ROUTES.ALL_DASHBOARD,
@@ -40,7 +40,7 @@ function DashboardBreadcrumbs(): JSX.Element {
} else {
safeNavigate(ROUTES.ALL_DASHBOARD);
}
}, [safeNavigate, selectedDashboard?.updatedAt]);
}, [safeNavigate, dashboardData?.updatedAt]);
return (
<div className="dashboard-breadcrumbs">

View File

@@ -35,15 +35,15 @@ jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (
selector?: (s: {
selectedDashboard: { locked: boolean } | undefined;
}) => { selectedDashboard: { locked: boolean } },
): { selectedDashboard: { locked: boolean } } => {
const mockState = { selectedDashboard: { locked: false } };
dashboardData: { locked: boolean } | undefined;
}) => { dashboardData: { locked: boolean } },
): { dashboardData: { locked: boolean } } => {
const mockState = { dashboardData: { locked: false } };
return selector ? selector(mockState) : mockState;
},
selectIsDashboardLocked: (s: {
selectedDashboard: { locked: boolean } | undefined;
}): boolean => s.selectedDashboard?.locked ?? false,
dashboardData: { locked: boolean } | undefined;
}): boolean => s.dashboardData?.locked ?? false,
}));
jest.mock('hooks/useNotifications', () => ({

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

@@ -24,9 +24,7 @@ function ExportPanelContainer({
}: ExportPanelProps): JSX.Element {
const { t } = useTranslation(['dashboard']);
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
null,
);
const [dashboardId, setDashboardId] = useState<string | null>(null);
const {
data,
@@ -55,17 +53,17 @@ function ExportPanelContainer({
const handleExportClick = useCallback((): void => {
const currentSelectedDashboard = data?.data?.find(
({ id }) => id === selectedDashboardId,
({ id }) => id === dashboardId,
);
onExport(currentSelectedDashboard || null, false);
}, [data, selectedDashboardId, onExport]);
}, [data, dashboardId, onExport]);
const handleSelect = useCallback(
(selectedDashboardValue: string): void => {
setSelectedDashboardId(selectedDashboardValue);
(selectedDashboardId: string): void => {
setDashboardId(selectedDashboardId);
},
[setSelectedDashboardId],
[setDashboardId],
);
const handleNewDashboard = useCallback(async () => {
@@ -85,10 +83,7 @@ function ExportPanelContainer({
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
const isDisabled =
isAllDashboardsLoading ||
!options?.length ||
!selectedDashboardId ||
isLoading;
isAllDashboardsLoading || !options?.length || !dashboardId || isLoading;
return (
<Wrapper direction="vertical">
@@ -101,7 +96,7 @@ function ExportPanelContainer({
showSearch
loading={isDashboardLoading}
disabled={isDashboardLoading}
value={selectedDashboardId}
value={dashboardId}
onSelect={handleSelect}
filterOption={filterOptions}
/>

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';
@@ -368,6 +377,7 @@ function FormAlertRules({
redirectWithQueryBuilderData(query);
};
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const validatePromParams = useCallback((): boolean => {
let retval = true;
@@ -533,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);
@@ -641,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', {

View File

@@ -27,7 +27,7 @@ export default function DashboardEmptyState(): JSX.Element {
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
@@ -43,7 +43,7 @@ export default function DashboardEmptyState(): JSX.Element {
}
const userRole: ROLES | null =
selectedDashboard?.createdBy === user?.email
dashboardData?.createdBy === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
@@ -52,9 +52,9 @@ export default function DashboardEmptyState(): JSX.Element {
const onEmptyWidgetHandler = useCallback(() => {
setIsPanelTypeSelectionModalOpen(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
dashboardId: dashboardData?.id,
dashboardName: dashboardData?.data.title,
numberOfPanels: dashboardData?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);

View File

@@ -91,7 +91,7 @@ function FullView({
setCurrentGraphRef(fullViewRef);
}, [setCurrentGraphRef]);
const { selectedDashboard, setColumnWidths } = useDashboardStore();
const { dashboardData, setColumnWidths } = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const onColumnWidthsChange = useCallback(
@@ -166,7 +166,7 @@ function FullView({
enableDrillDown,
widget,
setRequestData,
selectedDashboard,
dashboardData,
selectedPanelType,
});
@@ -344,7 +344,7 @@ function FullView({
<>
<QueryBuilderV2
panelType={selectedPanelType}
version={selectedDashboard?.data?.version || 'v3'}
version={dashboardData?.data?.version || 'v3'}
isListViewPanel={selectedPanelType === PANEL_TYPES.LIST}
signalSourceChangeEnabled
// filterConfigs={filterConfigs}

View File

@@ -19,7 +19,7 @@ export interface DrilldownQueryProps {
widget: Widgets;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
enableDrillDown: boolean;
selectedDashboard: Dashboard | undefined;
dashboardData: Dashboard | undefined;
selectedPanelType: PANEL_TYPES;
}
@@ -34,7 +34,7 @@ const useDrilldown = ({
enableDrillDown,
widget,
setRequestData,
selectedDashboard,
dashboardData,
selectedPanelType,
}: DrilldownQueryProps): UseDrilldownReturn => {
const isMounted = useRef(false);
@@ -60,11 +60,11 @@ const useDrilldown = ({
isMounted.current = true;
}, [widget, enableDrillDown, compositeQuery, redirectWithQueryBuilderData]);
const dashboardEditView = selectedDashboard?.id
const dashboardEditView = dashboardData?.id
? generateExportToDashboardLink({
query: currentQuery,
panelType: selectedPanelType,
dashboardId: selectedDashboard?.id || '',
dashboardId: dashboardData?.id || '',
widgetId: widget.id,
})
: '';

View File

@@ -163,13 +163,13 @@ const mockProps: WidgetGraphComponentProps = {
// Mock useDashabord hook
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: {
dashboardData: {
data: {
variables: [],
},
},
setLayouts: jest.fn(),
setSelectedDashboard: jest.fn(),
setDashboardData: jest.fn(),
setColumnWidths: jest.fn(),
}),
}));

View File

@@ -103,8 +103,8 @@ function WidgetGraphComponent({
const {
setLayouts,
selectedDashboard,
setSelectedDashboard,
dashboardData,
setDashboardData,
setColumnWidths,
} = useDashboardStore();
@@ -125,33 +125,33 @@ function WidgetGraphComponent({
const updateDashboardMutation = useUpdateDashboard();
const onDeleteHandler = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
const updatedWidgets = dashboardData?.data?.widgets?.filter(
(e) => e.id !== widget.id,
);
const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
dashboardData.data.layout?.filter((e) => e.i !== widget.id) || [];
const updatedSelectedDashboard: Props = {
const updatedDashboardData: Props = {
data: {
...selectedDashboard.data,
...dashboardData.data,
widgets: updatedWidgets,
layout: updatedLayout,
},
id: selectedDashboard.id,
id: dashboardData.id,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
updateDashboardMutation.mutateAsync(updatedDashboardData, {
onSuccess: (updatedDashboard) => {
if (setLayouts) {
setLayouts(updatedDashboard.data?.data?.layout || []);
}
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setDashboardData && updatedDashboard.data) {
setDashboardData(updatedDashboard.data);
}
setDeleteModal(false);
},
@@ -159,35 +159,35 @@ function WidgetGraphComponent({
};
const onCloneHandler = async (): Promise<void> => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const uuid = v4();
// this is added to make sure the cloned panel is of the same dimensions as the original one
const originalPanelLayout = selectedDashboard.data.layout?.find(
const originalPanelLayout = dashboardData.data.layout?.find(
(l) => l.i === widget.id,
);
const newLayoutItem = placeWidgetAtBottom(
uuid,
selectedDashboard?.data.layout || [],
dashboardData?.data.layout || [],
originalPanelLayout?.w || 6,
originalPanelLayout?.h || 6,
);
const layout = [...(selectedDashboard.data.layout || []), newLayoutItem];
const layout = [...(dashboardData.data.layout || []), newLayoutItem];
updateDashboardMutation.mutateAsync(
{
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
layout,
widgets: [
...(selectedDashboard.data.widgets || []),
...(dashboardData.data.widgets || []),
{
...{
...widget,
@@ -202,8 +202,8 @@ function WidgetGraphComponent({
if (setLayouts) {
setLayouts(updatedDashboard.data?.data?.layout || []);
}
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setDashboardData && updatedDashboard.data) {
setDashboardData(updatedDashboard.data);
}
notifications.success({
message: 'Panel cloned successfully, redirecting to new copy.',

View File

@@ -70,16 +70,16 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
useIsFetching([REACT_QUERY_KEY.DASHBOARD_BY_ID]) > 0;
const {
selectedDashboard,
dashboardData,
layouts,
setLayouts,
panelMap,
setPanelMap,
setSelectedDashboard,
setDashboardData,
columnWidths,
} = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const { data } = selectedDashboard || {};
const { data } = dashboardData || {};
const { pathname } = useLocation();
const dispatch = useDispatch();
@@ -124,7 +124,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
}
const userRole: ROLES | null =
selectedDashboard?.createdBy === user?.email
dashboardData?.createdBy === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
@@ -146,27 +146,27 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Dashboard Detail: Opened', {
dashboardId: selectedDashboard?.id,
dashboardId: dashboardData?.id,
dashboardName: data.title,
numberOfPanels: data.widgets?.length,
numberOfVariables: Object.keys(dashboardVariables).length || 0,
});
logEventCalledRef.current = true;
}
}, [dashboardVariables, data, selectedDashboard?.id]);
}, [dashboardVariables, data, dashboardData?.id]);
const onSaveHandler = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const updatedDashboard: Props = {
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
panelMap: { ...currentPanelMap },
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
widgets: selectedDashboard?.data?.widgets?.map((widget) => {
widgets: dashboardData?.data?.widgets?.map((widget) => {
if (columnWidths?.[widget.id]) {
return {
...widget,
@@ -184,7 +184,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
if (updatedDashboard.data.data.layout) {
setLayouts(sortLayout(updatedDashboard.data.data.layout));
}
setSelectedDashboard(updatedDashboard.data);
setDashboardData(updatedDashboard.data);
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
}
},
@@ -243,7 +243,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
dashboardLayout &&
Array.isArray(dashboardLayout) &&
dashboardLayout.length > 0 &&
hasColumnWidthsChanged(columnWidths, selectedDashboard);
hasColumnWidthsChanged(columnWidths, dashboardData);
if (shouldSaveLayout || shouldSaveColumnWidths) {
onSaveHandler();
@@ -253,7 +253,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const onSettingsModalSubmit = (): void => {
const newTitle = form.getFieldValue('title');
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
@@ -261,7 +261,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return;
}
const currentWidget = selectedDashboard?.data?.widgets?.find(
const currentWidget = dashboardData?.data?.widgets?.find(
(e) => e.id === currentSelectRowId,
);
@@ -269,25 +269,25 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return;
}
const updatedWidgets = selectedDashboard?.data?.widgets?.map((e) =>
const updatedWidgets = dashboardData?.data?.widgets?.map((e) =>
e.id === currentSelectRowId ? { ...e, title: newTitle } : e,
);
const updatedSelectedDashboard: Props = {
id: selectedDashboard.id,
const updatedDashboardData: Props = {
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
widgets: updatedWidgets,
},
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
updateDashboardMutation.mutateAsync(updatedDashboardData, {
onSuccess: (updatedDashboard) => {
if (setLayouts) {
setLayouts(updatedDashboard.data?.data?.layout || []);
}
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setDashboardData && updatedDashboard.data) {
setDashboardData(updatedDashboard.data);
}
if (setPanelMap) {
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
@@ -311,7 +311,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
}, [currentSelectRowId, form, widgets]);
const handleRowCollapse = (id: string): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const { updatedLayout, updatedPanelMap } = applyRowCollapse(
@@ -343,7 +343,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
};
const handleRowDelete = (): void => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
@@ -351,34 +351,33 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return;
}
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
const updatedWidgets = dashboardData?.data?.widgets?.filter(
(e) => e.id !== currentSelectRowId,
);
const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== currentSelectRowId) ||
[];
dashboardData.data.layout?.filter((e) => e.i !== currentSelectRowId) || [];
const updatedPanelMap = { ...currentPanelMap };
delete updatedPanelMap[currentSelectRowId];
const updatedSelectedDashboard: Props = {
id: selectedDashboard.id,
const updatedDashboardData: Props = {
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
widgets: updatedWidgets,
layout: updatedLayout,
panelMap: updatedPanelMap,
},
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
updateDashboardMutation.mutateAsync(updatedDashboardData, {
onSuccess: (updatedDashboard) => {
if (setLayouts) {
setLayouts(updatedDashboard.data?.data?.layout || []);
}
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setDashboardData && updatedDashboard.data) {
setDashboardData(updatedDashboard.data);
}
if (setPanelMap) {
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
@@ -390,10 +389,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
};
const isDashboardEmpty = useMemo(
() =>
selectedDashboard?.data.layout
? selectedDashboard?.data.layout?.length === 0
: true,
[selectedDashboard],
dashboardData?.data.layout ? dashboardData?.data.layout?.length === 0 : true,
[dashboardData],
);
let isDataAvailableInAnyWidget = false;
@@ -512,7 +509,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
headerMenuList={widgetActions}
variables={dashboardVariables}
// version={selectedDashboard?.data?.version}
// version={dashboardData?.data?.version}
version={ENTITY_VERSION_V5}
onDragSelect={onDragSelect}
dataAvailable={checkIfDataExists}

View File

@@ -42,14 +42,14 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
const userRole: ROLES | null =
selectedDashboard?.createdBy === user?.email
dashboardData?.createdBy === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
const [addPanelPermission] = useComponentPermission(permissions, userRole);
@@ -87,11 +87,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
icon={<Plus size={14} />}
onClick={(): void => {
// TODO: @AshwinBhatkal Simplify this check in cleanup of https://github.com/SigNoz/engineering-pod/issues/3953
if (!selectedDashboard?.id) {
if (!dashboardData?.id) {
return;
}
setSelectedRowWidgetId(selectedDashboard.id, id);
setSelectedRowWidgetId(dashboardData.id, id);
setIsPanelTypeSelectionModalOpen(true);
}}
>

View File

@@ -121,7 +121,7 @@ function useNavigateToExplorerPages(): (
) => Promise<{
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
}> {
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
const { notifications } = useNotifications();
return useCallback(
@@ -143,7 +143,7 @@ function useNavigateToExplorerPages(): (
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[selectedDashboard, notifications],
[dashboardData, notifications],
);
}

View File

@@ -20,7 +20,7 @@ interface UseUpdatedQueryOptions {
panelTypes: PANEL_TYPES;
timePreferance: timePreferenceType;
};
selectedDashboard?: any;
dashboardData?: any;
}
interface UseUpdatedQueryResult {
@@ -44,7 +44,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
const getUpdatedQuery = useCallback(
async ({
widgetConfig,
selectedDashboard,
dashboardData,
}: UseUpdatedQueryOptions): Promise<Query> => {
// Prepare query payload with resolved variables
const { queryPayload } = prepareQueryRangePayloadV5({
@@ -52,7 +52,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
graphType: getGraphType(widgetConfig.panelTypes),
selectedTime: widgetConfig.timePreferance,
globalSelectedInterval,
variables: getDashboardVariables(selectedDashboard?.data?.variables),
variables: getDashboardVariables(dashboardData?.data?.variables),
originalGraphType: widgetConfig.panelTypes,
dynamicVariables: dashboardDynamicVariables,
});

View File

@@ -149,16 +149,16 @@ export function extractQueryNamesFromExpression(expression: string): string[] {
export const hasColumnWidthsChanged = (
columnWidths: Record<string, Record<string, number>>,
selectedDashboard?: Dashboard,
dashboardData?: Dashboard,
): boolean => {
// If no column widths stored, no changes
if (isEmpty(columnWidths) || !selectedDashboard) {
if (isEmpty(columnWidths) || !dashboardData) {
return false;
}
// Check each widget's column widths
return Object.keys(columnWidths).some((widgetId) => {
const dashboardWidget = selectedDashboard?.data?.widgets?.find(
const dashboardWidget = dashboardData?.data?.widgets?.find(
(widget) => widget.id === widgetId,
) as Widgets;

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

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

@@ -93,8 +93,8 @@ jest.mock('hooks/useDarkMode', () => ({
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { selectedDashboard: undefined } => ({
selectedDashboard: undefined,
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));

View File

@@ -106,7 +106,7 @@ describe('LogsPanelComponent', () => {
<PreferenceContextProvider>
<NewWidget
dashboardId=""
selectedDashboard={undefined}
dashboardData={undefined}
selectedGraph={PANEL_TYPES.LIST}
/>
</PreferenceContextProvider>

View File

@@ -30,7 +30,7 @@ function LeftContainer({
setRequestData,
setQueryResponse,
enableDrillDown = false,
selectedDashboard,
dashboardData,
isNewPanel = false,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
@@ -79,8 +79,8 @@ function LeftContainer({
isLoadingQueries={queryResponse.isFetching}
selectedWidget={selectedWidget}
dashboardVersion={ENTITY_VERSION_V5}
dashboardId={selectedDashboard?.id}
dashboardName={selectedDashboard?.data.title}
dashboardId={dashboardData?.id}
dashboardName={dashboardData?.data.title}
isNewPanel={isNewPanel}
/>
{selectedGraph === PANEL_TYPES.LIST && (

View File

@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
<PreferenceContextProvider>
<NewWidget
dashboardId=""
selectedDashboard={undefined}
dashboardData={undefined}
selectedGraph={PANEL_TYPES.BAR}
/>
</PreferenceContextProvider>
@@ -378,7 +378,7 @@ describe('when switching to BAR panel type', () => {
<DashboardBootstrapWrapper dashboardId="">
<NewWidget
dashboardId=""
selectedDashboard={undefined}
dashboardData={undefined}
selectedGraph={PANEL_TYPES.BAR}
/>
</DashboardBootstrapWrapper>,

View File

@@ -86,7 +86,7 @@ import {
import './NewWidget.styles.scss';
function NewWidget({
selectedDashboard,
dashboardData,
dashboardId,
selectedGraph,
enableDrillDown = false,
@@ -135,7 +135,7 @@ function NewWidget({
[selectedGraph, globalSelectedInterval, isLogsQuery],
);
const { widgets = [] } = selectedDashboard?.data || {};
const { widgets = [] } = dashboardData?.data || {};
const query = useUrlQuery();
@@ -154,9 +154,9 @@ function NewWidget({
if (!logEventCalledRef.current) {
logEvent('Panel Edit: Page visited', {
panelType: selectedWidget?.panelTypes,
dashboardId: selectedDashboard?.id,
dashboardId: dashboardData?.id,
widgetId: selectedWidget?.id,
dashboardName: selectedDashboard?.data.title,
dashboardName: dashboardData?.data.title,
isNewPanel: !!isWidgetNotPresent,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
});
@@ -362,7 +362,7 @@ function NewWidget({
const updateDashboardMutation = useUpdateDashboard();
const { afterWidgets, preWidgets } = useMemo(() => {
if (!selectedDashboard) {
if (!dashboardData) {
return {
selectedWidget: {} as Widgets,
preWidgets: [],
@@ -372,21 +372,18 @@ function NewWidget({
const widgetId = query.get('widgetId');
const selectedWidgetIndex = getSelectedWidgetIndex(
selectedDashboard,
widgetId,
);
const selectedWidgetIndex = getSelectedWidgetIndex(dashboardData, widgetId);
const preWidgets = getPreviousWidgets(selectedDashboard, selectedWidgetIndex);
const preWidgets = getPreviousWidgets(dashboardData, selectedWidgetIndex);
const afterWidgets = getNextWidgets(selectedDashboard, selectedWidgetIndex);
const afterWidgets = getNextWidgets(dashboardData, selectedWidgetIndex);
const selectedWidget = (selectedDashboard.data.widgets || [])[
const selectedWidget = (dashboardData.data.widgets || [])[
selectedWidgetIndex || 0
];
return { selectedWidget, preWidgets, afterWidgets };
}, [selectedDashboard, query]);
}, [dashboardData, query]);
// this loading state is to take care of mismatch in the responses for table and other panels
// hence while changing the query contains the older value and the processing logic fails
@@ -483,12 +480,12 @@ function NewWidget({
}, [dashboardId, query, safeNavigate]);
const onClickSaveHandler = useCallback(() => {
if (!selectedDashboard) {
if (!dashboardData) {
return;
}
const widgetId = query.get('widgetId') || '';
let updatedLayout = selectedDashboard.data.layout || [];
let updatedLayout = dashboardData.data.layout || [];
const selectedRowWidgetId = getSelectedRowWidgetId(dashboardId);
@@ -522,10 +519,10 @@ function NewWidget({
const adjustedQueryForV5 = adjustQueryForV5(currentQuery);
const dashboard: Props = {
id: selectedDashboard.id,
id: dashboardData.id,
data: {
...selectedDashboard.data,
...dashboardData.data,
widgets: isNewDashboard
? [
...afterWidgets,
@@ -603,7 +600,7 @@ function NewWidget({
},
});
}, [
selectedDashboard,
dashboardData,
query,
isNewDashboard,
afterWidgets,
@@ -672,9 +669,9 @@ function NewWidget({
const onSaveDashboard = useCallback((): void => {
logEvent('Panel Edit: Save changes', {
panelType: selectedWidget.panelTypes,
dashboardId: selectedDashboard?.id,
dashboardId: dashboardData?.id,
widgetId: selectedWidget.id,
dashboardName: selectedDashboard?.data.title,
dashboardName: dashboardData?.data.title,
queryType: currentQuery.queryType,
isNewPanel,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
@@ -869,7 +866,7 @@ function NewWidget({
<OverlayScrollbar>
{selectedWidget && (
<LeftContainer
selectedDashboard={selectedDashboard}
dashboardData={dashboardData}
selectedGraph={graphType}
selectedLogFields={selectedLogFields}
setSelectedLogFields={setSelectedLogFields}

View File

@@ -10,7 +10,7 @@ import { timePreferance } from './RightContainer/timeItems';
export interface NewWidgetProps {
dashboardId: string;
selectedDashboard: Dashboard | undefined;
dashboardData: Dashboard | undefined;
selectedGraph: PANEL_TYPES;
enableDrillDown?: boolean;
}
@@ -34,7 +34,7 @@ export interface WidgetGraphProps {
>
>;
enableDrillDown?: boolean;
selectedDashboard: Dashboard | undefined;
dashboardData: Dashboard | undefined;
isNewPanel?: boolean;
}

View File

@@ -240,7 +240,7 @@ describe('InviteTeamMembers', () => {
describe('Validation callout on Complete', () => {
it('shows the correct callout message for each combination of email/role validity', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const user = userEvent.setup({ pointerEventsCheck: 0, delay: null });
renderComponent();
const removeButtons = screen.getAllByRole('button', {
@@ -302,10 +302,10 @@ describe('InviteTeamMembers', () => {
screen.queryByText(/please enter valid emails and select roles/i),
).not.toBeInTheDocument();
});
});
}, 15000);
it('treats whitespace as untouched, clears the callout on fix-and-resubmit, and clears role error on role select', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const user = userEvent.setup({ pointerEventsCheck: 0, delay: null });
renderComponent();
const removeButtons = screen.getAllByRole('button', {
@@ -365,7 +365,7 @@ describe('InviteTeamMembers', () => {
await waitFor(() => expect(mockOnNext).toHaveBeenCalledTimes(1), {
timeout: 1200,
});
});
}, 15000);
it('disables the Send Invites button when all rows are untouched (empty)', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });

View File

@@ -46,18 +46,16 @@ describe('CreateEdit Modal', () => {
// Tooltip mouseEnterDelay timers it triggers on the Configure button.
fireEvent.click(configureButtons[0]);
await waitFor(() => {
expect(screen.getByText(/edit google authentication/i)).toBeInTheDocument();
});
expect(
await screen.findByText(/edit google authentication/i),
).toBeInTheDocument();
const backButton = screen.getByRole('button', { name: /back/i });
fireEvent.click(backButton);
await waitFor(() => {
expect(
screen.getByText(/configure authentication method/i),
).toBeInTheDocument();
});
expect(
await screen.findByText(/configure authentication method/i),
).toBeInTheDocument();
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ const useBaseAggregateOptions = ({
getUpdatedQuery,
isLoading: isResolveQueryLoading,
} = useUpdatedQuery();
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
useEffect(() => {
if (!aggregateData) {
@@ -79,7 +79,7 @@ const useBaseAggregateOptions = ({
panelTypes: panelType || PANEL_TYPES.TIME_SERIES,
timePreferance: 'GLOBAL_TIME',
},
selectedDashboard,
dashboardData,
});
setResolvedQuery(updatedQuery);
};

View File

@@ -14,7 +14,7 @@ jest.mock('react-router-dom', () => ({
// Mock useDashabord hook
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: {
dashboardData: {
data: {
variables: [],
},

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ export function useDashboardBootstrap(
);
const {
setSelectedDashboard,
setDashboardData,
setLayouts,
setPanelMap,
resetDashboardStore,
@@ -65,7 +65,7 @@ export function useDashboardBootstrap(
transformDashboardVariables,
} = useTransformDashboardVariables(dashboardId);
// Keep the external variables store in sync with selectedDashboard
// Keep the external variables store in sync with dashboardData
useDashboardVariablesSync(dashboardId);
const dashboardQuery = useDashboardQuery(dashboardId);
@@ -88,7 +88,7 @@ export function useDashboardBootstrap(
if (variables) {
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
}
setSelectedDashboard(updatedDashboardData);
setDashboardData(updatedDashboardData);
dashboardRef.current = updatedDashboardData;
setLayouts(sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)));
setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {}));
@@ -107,7 +107,7 @@ export function useDashboardBootstrap(
title: t('dashboard_has_been_updated'),
content: t('do_you_want_to_refresh_the_dashboard'),
onOk() {
setSelectedDashboard(updatedDashboardData);
setDashboardData(updatedDashboardData);
const { maxTime, minTime } = getMinMaxForSelectedTime(
globalTime.selectedTime,

View File

@@ -13,21 +13,21 @@ import { useDashboardVariablesSelector } from './useDashboardVariables';
/**
* Keeps the external variables store in sync with the zustand dashboard store.
* When selectedDashboard changes, propagates variable updates to the variables store.
* When dashboardData changes, propagates variable updates to the variables store.
*/
export function useDashboardVariablesSync(dashboardId: string): void {
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
const selectedDashboard = useDashboardStore(
(s: DashboardStore) => s.selectedDashboard,
const dashboardData = useDashboardStore(
(s: DashboardStore) => s.dashboardData,
);
useEffect(() => {
const updatedVariables = selectedDashboard?.data.variables || {};
const updatedVariables = dashboardData?.data.variables || {};
if (savedDashboardId !== dashboardId) {
setDashboardVariablesStore({ dashboardId, variables: updatedVariables });
} else if (!isEqual(dashboardVariables, updatedVariables)) {
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
}
}, [selectedDashboard]); // eslint-disable-line react-hooks/exhaustive-deps
}, [dashboardData]); // eslint-disable-line react-hooks/exhaustive-deps
}

View File

@@ -1,7 +1,7 @@
import { useMutation } from 'react-query';
import locked from 'api/v1/dashboards/id/lock';
import {
getSelectedDashboard,
getDashboardData,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { useErrorModal } from 'providers/ErrorModalProvider';
@@ -13,13 +13,11 @@ import APIError from 'types/api/error';
*/
export function useLockDashboard(): (value: boolean) => Promise<void> {
const { showErrorModal } = useErrorModal();
const { setSelectedDashboard } = useDashboardStore();
const { setDashboardData } = useDashboardStore();
const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
setSelectedDashboard((prev) =>
prev ? { ...prev, locked: props.lock } : prev,
);
setDashboardData((prev) => (prev ? { ...prev, locked: props.lock } : prev));
},
onError: (error) => {
showErrorModal(error as APIError);
@@ -27,11 +25,11 @@ export function useLockDashboard(): (value: boolean) => Promise<void> {
});
return async (value: boolean): Promise<void> => {
const selectedDashboard = getSelectedDashboard();
if (selectedDashboard) {
const dashboardData = getDashboardData();
if (dashboardData) {
try {
await lockDashboard({
id: selectedDashboard.id,
id: dashboardData.id,
lock: value,
});
} catch (error) {

View File

@@ -12,11 +12,11 @@ import { useDashboardVariablesByType } from './useDashboardVariablesByType';
*/
export function useWidgetsByDynamicVariableId(): Record<string, string[]> {
const dynamicVariables = useDashboardVariablesByType('DYNAMIC', 'values');
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
return useMemo(() => {
const widgets =
selectedDashboard?.data?.widgets?.filter(
dashboardData?.data?.widgets?.filter(
(widget) => widget.panelTypes !== PANEL_GROUP_TYPES.ROW,
) || [];
@@ -24,5 +24,5 @@ export function useWidgetsByDynamicVariableId(): Record<string, string[]> {
dynamicVariables,
widgets as Widgets[],
);
}, [selectedDashboard, dynamicVariables]);
}, [dashboardData, dynamicVariables]);
}

View File

@@ -56,7 +56,7 @@ jest.mock('lib/dashboardVariables/getDashboardVariables', () => ({
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): unknown => ({ selectedDashboard: undefined }),
useDashboardStore: (): unknown => ({ dashboardData: undefined }),
}));
jest.mock('utils/getGraphType', () => ({

View File

@@ -33,7 +33,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const { notifications } = useNotifications();
const { selectedDashboard } = useDashboardStore();
const { dashboardData } = useDashboardStore();
const { dashboardVariables } = useDashboardVariables();
const dashboardDynamicVariables = useDashboardVariablesByType(
@@ -49,8 +49,8 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
if (caller === 'panelView') {
logEvent('Panel Edit: Create alert', {
panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.id,
dashboardName: dashboardData?.data?.title,
dashboardId: dashboardData?.id,
widgetId: widget.id,
queryType: widget.query.queryType,
});
@@ -58,8 +58,8 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
logEvent('Dashboard Detail: Panel action', {
action: MenuItemKeys.CreateAlerts,
panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.id,
dashboardName: dashboardData?.data?.title,
dashboardId: dashboardData?.id,
widgetId: widget.id,
queryType: widget.query.queryType,
});

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