- {state && (
- <>
- {state?.isProcessing ? (
-
diff --git a/apps/supernova/src/components/alerts/AlertsList.jsx b/apps/supernova/src/components/alerts/AlertsList.jsx
index 471a4121b..a0253bd63 100644
--- a/apps/supernova/src/components/alerts/AlertsList.jsx
+++ b/apps/supernova/src/components/alerts/AlertsList.jsx
@@ -3,103 +3,72 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useMemo, useState, useRef, useCallback, useEffect } from "react"
-import { DataGrid, DataGridHeadCell, DataGridRow, DataGridCell, Icon, Stack } from "@cloudoperators/juno-ui-components"
+import React from "react"
+import {
+ DataGrid,
+ DataGridHeadCell,
+ DataGridRow,
+ DataGridCell,
+ Icon,
+ Stack,
+ Spinner,
+ useEndlessScrollList,
+} from "@cloudoperators/juno-ui-components"
import Alert from "./Alert"
-import { useAlertsItemsFiltered, useAlertsActions } from "../StoreProvider"
-import { useBoundQuery } from "../../hooks/useBoundQuery"
+import { useAlertsItemsFiltered } from "../StoreProvider"
const AlertsList = () => {
- const [visibleAmount, setVisibleAmount] = useState(20)
- const [isAddingItems, setIsAddingItems] = useState(false)
- const timeoutRef = React.useRef(null)
-
const itemsFiltered = useAlertsItemsFiltered()
- const { setAlertsData } = useAlertsActions()
-
- const { data, isLoading } = useBoundQuery("alerts")
-
- useEffect(() => {
- if (data) {
- setAlertsData({ items: data.alerts, counts: data.counts })
- }
- }, [data])
-
- const alertsSorted = useMemo(() => {
- if (itemsFiltered) {
- return itemsFiltered.slice(0, visibleAmount)
- }
- }, [itemsFiltered, visibleAmount])
- React.useEffect(() => {
- return () => clearTimeout(timeoutRef.current) // clear when component is unmounted
- }, [])
-
- // endless scroll observer
- const observer = useRef()
- const lastListElementRef = useCallback(
- (node) => {
- // no fetch if loading original data
- if (isLoading || isAddingItems) return
- if (observer.current) observer.current.disconnect()
- observer.current = new IntersectionObserver((entries) => {
- console.debug("IntersectionObserver: callback")
- if (entries[0].isIntersecting && visibleAmount <= alertsSorted.length) {
- // setVisibleAmount((prev) => prev + 10)
- clearTimeout(timeoutRef.current)
- setIsAddingItems(true)
- timeoutRef.current = setTimeout(() => {
- setIsAddingItems(false)
- setVisibleAmount((prev) => prev + 10)
- }, 500)
- }
- })
- if (node) observer.current.observe(node)
- },
- [isLoading, isAddingItems]
- )
+ const { scrollListItems, iterator } = useEndlessScrollList(itemsFiltered, {
+ loadingObject: (
+
+
+
+ Loading...
+
+
+
+
+ ),
+ refFunction: (ref) => (
+
+
+
+
+
+ ),
+ })
return (
- <>
-
-
-
-
- Region
- Service
- Description
- Firing Since
- Status
-
+
+
+
+
+ Region
+ Service
+ Description
+ Firing Since
+ Status
+
+
+
+ {scrollListItems?.length > 0 ? (
+ iterator.map((alert) => )
+ ) : (
+
+
+
+
+
+ We couldn't find anything. It's possible that the matching alerts are not active at the
+ moment, or the chosen filters could be overly limiting.
+
+
+
- {alertsSorted?.length > 0 ? (
- alertsSorted?.map((alert, index) => {
- if (alertsSorted.length === index + 1) {
- // DataRow in alerts muss implement forwardRef
- return
- }
- return
- })
- ) : (
-
-
-
-
-
- We couldn't find anything. It's possible that the matching alerts are not active at the
- moment, or the chosen filters could be overly limiting.
-
-
-
-
- )}
- {isAddingItems && (
-
- Loading ...
-
- )}
- >
+ )}
)
}
diff --git a/apps/supernova/src/components/alerts/AlertsTab.jsx b/apps/supernova/src/components/alerts/AlertsTab.jsx
new file mode 100644
index 000000000..11012571c
--- /dev/null
+++ b/apps/supernova/src/components/alerts/AlertsTab.jsx
@@ -0,0 +1,50 @@
+import React, { useEffect } from "react"
+import { Stack, Spinner } from "@cloudoperators/juno-ui-components"
+import { useBoundQuery } from "../../hooks/useBoundQuery"
+import AlertsList from "./AlertsList"
+import StatusBar from "../status/StatusBar"
+import Filters from "../filters/Filters"
+import { useActions } from "@cloudoperators/juno-messages-provider"
+import PredefinedFilters from "../filters/PredefinedFilters"
+import { useAlertsUpdatedAt, useAlertsTotalCounts, useAlertsActions } from "../StoreProvider"
+import { parseError } from "../../helpers"
+const AlertsTab = () => {
+ const totalCounts = useAlertsTotalCounts()
+ const updatedAt = useAlertsUpdatedAt()
+ const { setAlertsData } = useAlertsActions()
+ const { addMessage } = useActions()
+
+ // Fetch alerts data
+ const { data, isLoading, error } = useBoundQuery("alerts")
+ if (error) {
+ addMessage({
+ variant: "error",
+ text: parseError(error),
+ })
+ }
+ useEffect(() => {
+ if (data) {
+ setAlertsData({ items: data.alerts, counts: data.counts })
+ }
+ }, [data])
+
+ return (
+ <>
+ {isLoading ? (
+
+ Loading
+
+
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+ >
+ )
+}
+
+export default AlertsTab
diff --git a/apps/supernova/src/components/alerts/shared/AlertSilencesList.jsx b/apps/supernova/src/components/alerts/shared/AlertSilencesList.jsx
index f85ef1cf6..d8113e8fd 100644
--- a/apps/supernova/src/components/alerts/shared/AlertSilencesList.jsx
+++ b/apps/supernova/src/components/alerts/shared/AlertSilencesList.jsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useEffect } from "react"
+import React from "react"
import { DateTime } from "luxon"
import constants from "../../../constants"
import ExpireSilence from "../../silences/ExpireSilence"
@@ -11,14 +11,12 @@ import RecreateSilence from "../../silences/RecreateSilence"
import { Badge, DataGrid, DataGridCell, DataGridHeadCell, DataGridRow } from "@cloudoperators/juno-ui-components"
-import { useSilencesActions, useSilencesLocalItems } from "../../StoreProvider"
+import { useSilencesActions } from "../../StoreProvider"
const badgeVariant = (state) => {
switch (state) {
case constants.SILENCE_STATE_ACTIVE:
return "info"
- case constants.SILENCE_CREATING:
- return "warning"
default:
return "default"
}
@@ -26,7 +24,6 @@ const badgeVariant = (state) => {
const AlertSilencesList = ({ alert }) => {
const dateFormat = { ...DateTime.DATETIME_SHORT }
- const localItems = useSilencesLocalItems()
const { getSilencesForAlert } = useSilencesActions()
let silenceList = getSilencesForAlert(alert)
@@ -36,10 +33,6 @@ const AlertSilencesList = ({ alert }) => {
return time.toLocaleString(dateFormat)
}
- useEffect(() => {
- silenceList = getSilencesForAlert(alert)
- }, [localItems])
-
return (
<>
{silenceList.length > 0 && (
@@ -69,9 +62,7 @@ const AlertSilencesList = ({ alert }) => {
{
/// show the expire button if the silence is active or pending
// else show recreate button
- silence?.status?.state === constants.SILENCE_ACTIVE ||
- silence?.status?.state === constants.SILENCE_PENDING ||
- silence?.status?.state === constants.SILENCE_CREATING ? (
+ silence?.status?.state === constants.SILENCE_ACTIVE ? (
) : (
diff --git a/apps/supernova/src/components/filters/PredefinedFilters.jsx b/apps/supernova/src/components/filters/PredefinedFilters.jsx
index 9393db3d7..48f81a45c 100644
--- a/apps/supernova/src/components/filters/PredefinedFilters.jsx
+++ b/apps/supernova/src/components/filters/PredefinedFilters.jsx
@@ -7,7 +7,7 @@ import React, { useState } from "react"
import { Stack, TabNavigation, TabNavigationItem } from "@cloudoperators/juno-ui-components"
import { useActivePredefinedFilter, useFilterActions, usePredefinedFilters } from "../StoreProvider"
-import SilenceScheduledWrapper from "../silences/SilenceScheduledWrapper"
+import SilenceScheduled from "../silences/SilenceScheduled"
const PredefinedFilters = () => {
const { setActivePredefinedFilter } = useFilterActions()
@@ -31,7 +31,7 @@ const PredefinedFilters = () => {
)}
-
+
)
diff --git a/apps/supernova/src/components/silences/SilenceNew.jsx b/apps/supernova/src/components/silences/CreateSilence.jsx
similarity index 82%
rename from apps/supernova/src/components/silences/SilenceNew.jsx
rename to apps/supernova/src/components/silences/CreateSilence.jsx
index 673ad729a..9101a3c76 100644
--- a/apps/supernova/src/components/silences/SilenceNew.jsx
+++ b/apps/supernova/src/components/silences/CreateSilence.jsx
@@ -18,20 +18,24 @@ import {
} from "@cloudoperators/juno-ui-components"
import {
useSilencesExcludedLabels,
- useGlobalsApiEndpoint,
useSilencesActions,
useAlertEnrichedLabels,
useGlobalsUsername,
+ useSilencesItems,
} from "../StoreProvider"
-import { post } from "../../api/client"
import AlertDescription from "../alerts/shared/AlertDescription"
-import SilenceNewAdvanced from "./SilenceNewAdvanced"
-import { debounce } from "../../helpers"
+import { useActions } from "@cloudoperators/juno-messages-provider"
+import CreateSilenceAdvanced from "./CreateSilenceAdvanced"
import { DateTime } from "luxon"
import { latestExpirationDate, getSelectOptions, setupMatchers } from "./silenceHelpers"
import { parseError } from "../../helpers"
+
+import { debounce } from "../../helpers"
import constants from "../../constants"
+import { useQueryClient } from "@tanstack/react-query"
+import { useBoundMutation } from "../../hooks/useBoundMutation"
+
const validateForm = (values) => {
const minCommentLength = 3
const minUserNameLength = 1
@@ -60,25 +64,27 @@ const errorHelpText = (messages) => {
const DEFAULT_FORM_VALUES = { duration: "2", comment: "" }
-const SilenceNew = ({ alert, size, variant }) => {
- const apiEndpoint = useGlobalsApiEndpoint()
+const CreateSilence = ({ alert, size, variant }) => {
+ const queryClient = useQueryClient()
const excludedLabels = useSilencesExcludedLabels()
- const { addLocalItem, getMappingSilences } = useSilencesActions()
+ const { getMappingSilences, setSilences } = useSilencesActions()
const enrichedLabels = useAlertEnrichedLabels()
const user = useGlobalsUsername()
-
const [displayNewSilence, setDisplayNewSilence] = useState(false)
const [formState, setFormState] = useState(DEFAULT_FORM_VALUES)
const [expirationDate, setExpirationDate] = useState(null)
const [showValidation, setShowValidation] = useState({})
const [error, setError] = useState(null)
+ const { addMessage } = useActions()
+
+ const silences = useSilencesItems()
+
const [success, setSuccess] = useState(null)
// Initialize form state on modal open
// Removed alert from dependencies since we take an screenshot of the global state on opening the modal
// This is due to if the alert changes (e.g. the alert receives a new silenceBy) while the modal is open, the form state will be reset
useLayoutEffect(() => {
- if (!displayNewSilence) return
// reset form state with default values
setFormState({
...formState,
@@ -108,6 +114,41 @@ const SilenceNew = ({ alert, size, variant }) => {
return options.items
}, [expirationDate])
+ const { mutate: createSilence } = useBoundMutation("createSilences", {
+ onMutate: (data) => {
+ queryClient.cancelQueries("silences")
+
+ const newSilence = { ...data.silence, status: { state: constants.SILENCE_ACTIVE } }
+
+ const newSilences = [...silences, newSilence]
+
+ setSilences({
+ items: newSilences,
+ })
+
+ setDisplayNewSilence(false)
+ },
+
+ onSuccess: (data) => {
+ addMessage({
+ variant: "success",
+ text: `A silence object with id ${data?.silenceID} was created successfully. Please note that it may
+ take up to 5 minutes for the alert to show up as silenced.`,
+ })
+ },
+ onError: (error) => {
+ // add a error message in UI
+ addMessage({
+ variant: "error",
+ text: parseError(error),
+ })
+ },
+ onSettled: () => {
+ // Optionale zusätzliche Aktionen, wie das erneute Abrufen von Daten
+ queryClient.invalidateQueries(["silences"])
+ },
+ })
+
// debounce to prevent accidental double clicks from creating multiple silences
const onSubmitForm = debounce(() => {
setError(null)
@@ -134,24 +175,8 @@ const SilenceNew = ({ alert, size, variant }) => {
alertFingerprint: alert.fingerprint,
}
- // submit silence
- post(`${apiEndpoint}/silences`, {
- body: JSON.stringify(newSilence),
- })
- .then((data) => {
- setSuccess(data)
- if (data?.silenceID) {
- // add silence to local store
- addLocalItem({
- silence: newSilence,
- id: data.silenceID,
- type: "local",
- })
- }
- })
- .catch((error) => {
- setError(parseError(error))
- })
+ // calling createSilence with variable silence: newSilence
+ createSilence({ silence: newSilence })
}, 200)
const onInputChanged = ({ key, value }) => {
@@ -214,7 +239,7 @@ const SilenceNew = ({ alert, size, variant }) => {
{!success && (
<>
-
+
- )}
+
+ )}{" "}
>
)}
-
+ >
)
}
diff --git a/apps/supernova/src/components/silences/SilenceScheduledWrapper.jsx b/apps/supernova/src/components/silences/SilenceScheduledWrapper.jsx
deleted file mode 100644
index 6247f61b0..000000000
--- a/apps/supernova/src/components/silences/SilenceScheduledWrapper.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React, { useState } from "react"
-import SilenceScheduled from "./SilenceScheduled"
-
-import { MessagesProvider } from "@cloudoperators/juno-messages-provider"
-import { Button } from "@cloudoperators/juno-ui-components"
-import { useSilenceTemplates } from "../StoreProvider"
-
-const SilenceScheduledWrapper = () => {
- const templates = useSilenceTemplates()
- const [displayNewScheduledSilence, setDisplayNewScheduledSilence] = useState(false)
-
- // function which sets displayNewScheduledSilence to false
- const callbackOnClose = () => {
- setDisplayNewScheduledSilence(false)
- }
-
- return (
- <>
- {templates && templates?.length > 0 && (
-
-
- {displayNewScheduledSilence && }
-
- )}
- >
- )
-}
-
-export default SilenceScheduledWrapper
diff --git a/apps/supernova/src/components/silences/SilencesItem.jsx b/apps/supernova/src/components/silences/SilencesItem.jsx
index c426b7519..643c30664 100644
--- a/apps/supernova/src/components/silences/SilencesItem.jsx
+++ b/apps/supernova/src/components/silences/SilencesItem.jsx
@@ -43,8 +43,7 @@ const SilencesItem = ({ silence }, ref) => {
{
/// show the expire button only if the silence is active or pending
silence?.status?.state === constants.SILENCE_ACTIVE ||
- silence?.status?.state === constants.SILENCE_PENDING ||
- silence?.status?.state === constants.SILENCE_CREATING ? (
+ silence?.status?.state === constants.SILENCE_PENDING ? (
) : (
diff --git a/apps/supernova/src/components/silences/SilencesList.jsx b/apps/supernova/src/components/silences/SilencesList.jsx
index 68801a0f5..bd965868b 100644
--- a/apps/supernova/src/components/silences/SilencesList.jsx
+++ b/apps/supernova/src/components/silences/SilencesList.jsx
@@ -18,15 +18,11 @@ import {
useEndlessScrollList,
} from "@cloudoperators/juno-ui-components"
import constants from "../../constants"
-import {
- useSilencesItems,
- useSilencesActions,
- useSilencesRegEx,
- useSilencesStatus,
- useSilencesLocalItems,
-} from "../StoreProvider"
+import { useSilencesItems, useSilencesActions, useSilencesRegEx, useSilencesStatus } from "../StoreProvider"
import SilencesItem from "./SilencesItem"
import { useBoundQuery } from "../../hooks/useBoundQuery"
+import { parseError } from "../../helpers"
+import { useActions } from "@cloudoperators/juno-messages-provider"
const filtersStyles = `
bg-theme-background-lvl-1
@@ -40,17 +36,22 @@ const SilencesList = () => {
const [visibleSilences, setVisibleSilences] = useState(silences)
const status = useSilencesStatus()
const regEx = useSilencesRegEx()
- const localSilences = useSilencesLocalItems()
const { setSilences, setSilencesStatus, setSilencesRegEx } = useSilencesActions()
+ const { addMessage } = useActions()
+
+ const { data, isLoading, error } = useBoundQuery("silences")
- const { data } = useBoundQuery("silences")
+ if (error) {
+ addMessage({
+ variant: "error",
+ text: parseError(error),
+ })
+ }
useEffect(() => {
if (data) {
setSilences({
items: data?.silences,
- itemsHash: data?.silencesHash,
- itemsByState: data?.silencesBySate,
})
}
}, [data])
@@ -68,31 +69,8 @@ const SilencesList = () => {
filtered = filtered.filter((silence) => JSON.stringify(silence).toLowerCase().includes(regEx.toLowerCase))
}
- if (localSilences) {
- // when selected silences status is pending: if localSilence.status.state is creating add them to filtered
- if (status === constants.SILENCE_PENDING) {
- for (const [, localSilence] of Object.entries(localSilences)) {
- if (localSilence.status.state === constants.SILENCE_CREATING) {
- filtered.push(localSilence)
- }
- }
- }
-
- // when selected silences status is active: if silence.id is in localSilences add the localSilence to the shownSilences else the filtered silence
- if (status === constants.SILENCE_ACTIVE) {
- filtered = filtered.map((silence) => {
- for (const [, localSilence] of Object.entries(localSilences)) {
- if (silence.id === localSilence.id) {
- return localSilence
- }
- }
- return silence
- })
- }
- }
-
setVisibleSilences(filtered)
- }, [status, regEx, silences, localSilences])
+ }, [status, regEx, silences])
const handleSearchChange = (value) => {
// debounce setSearchTerm to avoid unnecessary re-renders
@@ -125,58 +103,67 @@ const SilencesList = () => {
return (
<>
-
-
-
- {
- handleSearchChange(text)
- }}
- onSearch={(text) => {
- setSilencesRegEx(text)
- }}
- onClear={() => {
- setSilencesRegEx(null)
- }}
- />
-
-
-
+ {isLoading ? (
+
+ Loading
+
+
+ ) : (
<>
-
- Time intervall
- Details
- State
- Action
-
- {scrollListItems?.length > 0 ? (
- iterator.map((silence) => )
- ) : (
-
-
-
-
- We couldn't find any matching silences.
-
-
-
- )}
+
+
+
+ {
+ handleSearchChange(text)
+ }}
+ onSearch={(text) => {
+ setSilencesRegEx(text)
+ }}
+ onClear={() => {
+ setSilencesRegEx(null)
+ }}
+ />
+
+
+
+ <>
+
+ Time intervall
+ Details
+ State
+ Action
+
+ {scrollListItems?.length > 0 ? (
+ iterator.map((silence) => )
+ ) : (
+
+
+
+
+ We couldn't find any matching silences.
+
+
+
+ )}
+ >
+
>
-
+ )}
>
)
}
diff --git a/apps/supernova/src/constants.js b/apps/supernova/src/constants.js
index 77fc79cb3..3214af257 100644
--- a/apps/supernova/src/constants.js
+++ b/apps/supernova/src/constants.js
@@ -7,8 +7,6 @@ const constants = {
SILENCE_ACTIVE: "active",
SILENCE_PENDING: "pending",
SILENCE_EXPIRED: "expired",
- SILENCE_CREATING: "creating",
- SILENCE_EXPIRING: "expiring",
}
export default constants
diff --git a/apps/supernova/src/hooks/useAlertmanagerAPI.js b/apps/supernova/src/hooks/useAlertmanagerAPI.js
deleted file mode 100644
index 635b74f1a..000000000
--- a/apps/supernova/src/hooks/useAlertmanagerAPI.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { useEffect } from "react"
-import {
- useAlertsActions,
- useUserIsActive,
- useSilencesActions,
- useSilencesLocalItems,
-} from "../components/StoreProvider"
-import AlertsWorker from "../workers/alerts.js?worker&inline"
-import SilencesWorker from "../workers/silences.js?worker&inline"
-
-const alertsWorker = new AlertsWorker()
-const silencesWorker = new SilencesWorker()
-
-const useAlertmanagerAPI = (apiEndpoint) => {
- const {
- setAlertsData,
- setIsLoading: setAlertsIsLoading,
- setIsUpdating: setAlertsIsUpdating,
- setError: setAlertsError,
- } = useAlertsActions()
-
- const isUserActive = useUserIsActive()
-
- const { setSilences, setIsUpdating: setSilencesIsUpdating, setError: setSilencesError } = useSilencesActions()
-
- // Setup web workers
- useEffect(() => {
- let cleanupAlertsWorker = () => alertsWorker.terminate()
- let cleanupSilencesWorker = () => silencesWorker.terminate()
-
- // receive messages from worker
- alertsWorker.onmessage = (e) => {
- const action = e.data.action
- switch (action) {
- case "ALERTS_UPDATE":
- console.debug("Worker::ALERT_UPDATE::", e.data)
- setAlertsData({ items: e.data.alerts, counts: e.data.counts })
- break
- case "ALERTS_FETCH_START":
- console.debug("Worker::ALERTS_FETCH_START::")
- setAlertsIsUpdating(true)
- break
- case "ALERTS_FETCH_END":
- console.debug("Worker::ALERTS_FETCH_END::")
- setAlertsIsUpdating(false)
- break
- case "ALERTS_FETCH_ERROR":
- console.debug("Worker::ALERTS_FETCH_ERROR::", e.data.error)
- setAlertsIsUpdating(false)
- // error comes as object string and have to be parsed
- setAlertsError(e.data.error)
- break
- }
- }
-
- // receive messages from worker
- silencesWorker.onmessage = (e) => {
- const action = e.data.action
- switch (action) {
- case "SILENCES_UPDATE":
- console.debug("Worker::SILENCES_UPDATE::", e.data)
- setSilences({
- items: e.data?.silences,
- itemsHash: e.data?.silencesHash,
- itemsByState: e.data?.silencesBySate,
- })
- break
- case "SILENCES_FETCH_START":
- console.debug("Worker::SILENCES_FETCH_START::")
- setSilencesIsUpdating(true)
- break
- case "SILENCES_FETCH_END":
- console.debug("Worker::SILENCES_FETCH_END::")
- setSilencesIsUpdating(false)
- break
- case "SILENCES_FETCH_ERROR":
- console.debug("Worker::SILENCES_FETCH_ERROR::", e.data.error)
- setSilencesIsUpdating(false)
- // error comes as object string and have to be parsed
- setSilencesError(e.data.error)
- break
- }
- }
-
- return () => {
- cleanupAlertsWorker()
- cleanupSilencesWorker()
- }
- }, [])
-
- // Reconfigure workers each time we get a new API endpoint
- useEffect(() => {
- if (!apiEndpoint) return
-
- setAlertsIsLoading(true)
- alertsWorker.postMessage({
- action: "ALERTS_CONFIGURE",
- fetchVars: { apiEndpoint, options: {} },
- debug: true,
- })
-
- silencesWorker.postMessage({
- action: "SILENCES_CONFIGURE",
- apiEndpoint,
- })
- }, [apiEndpoint])
-
- // Enable or disable watching in the workers based on user activity
- useEffect(() => {
- if (isUserActive === undefined) return
-
- alertsWorker.postMessage({
- action: "ALERTS_CONFIGURE",
- watch: isUserActive,
- })
-
- silencesWorker.postMessage({
- action: "SILENCES_CONFIGURE",
- watch: isUserActive,
- })
- }, [isUserActive])
-
- // Handle re-fetching silences when local items change
- const localItems = useSilencesLocalItems()
- useEffect(() => {
- // if we have no silences locally we don't need to refetch them otherwise
- // we will end up in an infinite loop
- if (!localItems || Object.keys(localItems).length <= 0) return
-
- // Use setTimeout to delay the worker call delayed by 10s
- setTimeout(() => {
- silencesWorker.postMessage({ action: "SILENCES_FETCH" })
- }, 10000)
-
- return () => {
- if (silencesWorker) {
- silencesWorker.terminate()
- }
- }
- }, [localItems])
-}
-
-export default useAlertmanagerAPI
diff --git a/apps/supernova/src/hooks/useBoundMutation.js b/apps/supernova/src/hooks/useBoundMutation.js
new file mode 100644
index 000000000..84c252690
--- /dev/null
+++ b/apps/supernova/src/hooks/useBoundMutation.js
@@ -0,0 +1,19 @@
+// useBoundMutation.js
+import { useMutation } from "@tanstack/react-query"
+import { MUTATION_FUNCTIONS } from "../api/mutationFunctions"
+import { useGlobalsApiEndpoint } from "../components/StoreProvider"
+
+export const useBoundMutation = (key, options = {}) => {
+ const endpoint = useGlobalsApiEndpoint()
+
+ const mutationFn = MUTATION_FUNCTIONS[key]
+ if (!mutationFn) {
+ throw new Error(`No mutation function mapped for key: ${key}`)
+ }
+
+ return useMutation({
+ mutationFn: (variables) => mutationFn({ ...variables, endpoint }),
+ onError: options?.onError,
+ ...options,
+ })
+}
diff --git a/apps/supernova/src/hooks/useBoundQuery.js b/apps/supernova/src/hooks/useBoundQuery.js
index d2db13b68..ed55666ec 100644
--- a/apps/supernova/src/hooks/useBoundQuery.js
+++ b/apps/supernova/src/hooks/useBoundQuery.js
@@ -4,7 +4,7 @@
*/
import { useQuery } from "@tanstack/react-query"
-import { QUERY_FUNCTIONS } from "../lib/queries/queryFunctions"
+import { QUERY_FUNCTIONS } from "../api/queryFunctions"
import { useGlobalsApiEndpoint } from "../components/StoreProvider"
export const useBoundQuery = (key, { options } = {}) => {
diff --git a/apps/supernova/src/lib/createSilencesSlice.jsx b/apps/supernova/src/lib/createSilencesSlice.jsx
index 0cb3fc00b..552f8dffe 100644
--- a/apps/supernova/src/lib/createSilencesSlice.jsx
+++ b/apps/supernova/src/lib/createSilencesSlice.jsx
@@ -3,16 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { produce } from "immer"
-import constants from "../constants"
-
const initialSilencesState = {
items: [],
- itemsHash: {},
- itemsByState: {},
excludedLabels: [],
updatedAt: null,
- localItems: {},
status: "active",
regEx: "",
@@ -124,153 +118,43 @@ const createSilencesSlice = (set, get, options) => ({
return get().silences.items.find((silence) => silence.id === id)
},
- setSilences: ({ items, itemsHash, itemsByState }) => {
+ setSilences: ({ items }) => {
if (!items) return
set((state) => ({
silences: {
...state.silences,
items: items,
- itemsHash: itemsHash,
- itemsByState: itemsByState,
updatedAt: Date.now(),
},
})),
false,
"silences.setSilencesData"
-
- // check if any local item can be removed
- get().silences.actions.updateLocalItems()
- },
- /*
- Save temporary created silences to be able to display which alert is silenced
- and who silenced it until the next alert fetch contains the silencedBy reference
- */
- addLocalItem: ({ silence, id, type }) => {
- // enforce silences with id and alertFingerprint
- if (!silence || !id || !type) return
- return set(
- produce((state) => {
- state.silences.localItems = {
- ...get().silences.localItems,
- [id]: {
- ...silence,
- id,
- type: type,
- },
- }
- }),
- false,
- "silences.addLocalItem"
- )
},
- /*
- Remove local silences which are already referenced by an alert
- */
- updateLocalItems: () => {
- const allSilences = get().silences.itemsHash
-
- const SilencesByState = get().silences.itemsByState
- let newLocalSilences = { ...get().silences.localItems }
- Object.keys(newLocalSilences).forEach((key) => {
- // if mapped to alert second logic
- if (!newLocalSilences[key]?.alertFingerprint) {
- // when newLocalSilences[key].silenceId with a creating state is in aktive SilencesByState, then remove it
- if (
- newLocalSilences[key]?.status?.state === constants.SILENCE_CREATING &&
- (SilencesByState?.active?.find((silence) => silence?.id === newLocalSilences[key]?.id) ||
- SilencesByState?.pending?.find((silence) => silence?.id === newLocalSilences[key]?.id))
- ) {
- newLocalSilences[key] = { ...newLocalSilences[key], remove: true }
- }
- // when newLocalSilences[key].silenceId with a expiring state is in expired SilencesByState, then remove it
- if (
- newLocalSilences[key]?.status?.state === constants.SILENCE_EXPIRING &&
- SilencesByState?.expired?.find((silence) => silence?.id === newLocalSilences[key]?.id)
- ) {
- newLocalSilences[key] = { ...newLocalSilences[key], remove: true }
- }
-
- // continue to next iteration
- return
- }
-
- const alert = get().alerts.actions.getAlertByFingerprint(newLocalSilences[key]?.alertFingerprint)
- // check if the alert has already the silence reference and if the extern silence already exists
- const silencedBy = alert?.status?.silencedBy
- if (silencedBy?.length > 0 && silencedBy?.includes(newLocalSilences[key]?.id) && allSilences[key]) {
- // mark to remove silence
- newLocalSilences[key] = { ...newLocalSilences[key], remove: true }
- }
- })
-
- // remove silences marked to remove
- const reducedLocalSilences = Object.keys(newLocalSilences)
- .filter((key) => !newLocalSilences[key]?.remove)
- .reduce((obj, key) => {
- obj[key] = newLocalSilences[key]
- return obj
- }, {})
-
- return set(
- produce((state) => {
- state.silences.localItems = reducedLocalSilences
- }),
- false,
- "silences.updateLocalItems"
- )
- },
/*
- Given an alert fingerprint, this function returns all silences referenced by silencingBy. It also
- check if there are local silences with the same alert fingerprint and return them as well.
+ Given an alert fingerprint, this function returns all silences referenced by silencingBy.
*/
getMappingSilences: (alert) => {
if (!alert) return
- const externalSilences = get().silences.itemsHash
+ const externalSilences = get().silences.items
let silencedBy = alert?.status?.silencedBy || []
// ensure silencedBy is an array
if (!Array.isArray(silencedBy)) silencedBy = [silencedBy]
- let mappingSilences = []
- silencedBy.forEach((id) => {
- if (externalSilences[id]) {
- mappingSilences.push(externalSilences[id])
- }
- })
- // add local silences
- let localSilences = get().silences.localItems
- Object.keys(localSilences).forEach((silenceID) => {
- // if there is already a silence with the same id, skip it and exists as external silence
- if (silencedBy.includes(silenceID) && externalSilences[silenceID]) return
- // if the local silence has the same alert fingerprint, add it to the mapping silences
- if (localSilences[silenceID]?.alertFingerprint === alert?.fingerprint) {
- mappingSilences.push(localSilences[silenceID])
- }
- })
+ const silencedBySet = new Set(silencedBy)
+ const mappingSilences = externalSilences.filter((silence) => silencedBySet.has(silence.id))
+
return mappingSilences
},
/*
- Return the state of an alert. If the alert is silenced by a local silence, the state is suppressed (processing)
- */
- getMappedState: (alert) => {
- if (!alert) return
- // get all silences (local and external)
- const silences = get().silences.actions.getMappingSilences(alert)
- // if there is a silence with type local, return suppressed (processing)
- if (silences?.find((silence) => silence?.type === "local")) {
- return { type: "suppressed", isProcessing: true }
- }
- return { type: alert?.status?.state, isProcessing: false }
- },
- /*
- Find all silences in itemsByState with key expired that matches all labels (key&value) from the alert but omit the labels that are excluded (excludedLabels)
+ Find all silences with key expired that matches all labels (key&value) from the alert but omit the labels that are excluded (excludedLabels)
*/
getExpiredSilences: (alert) => {
if (!alert) return
const alertLabels = alert?.labels || {}
- const silences = get().silences.itemsByState?.expired || []
+ const silences = get().silences.items.filter((silence) => silence?.status?.state === "expired") || []
const excludedLabels = get().silences.excludedLabels || []
const enrichedLabels = get().alerts.enrichedLabels || []
// combine the arrays containing the labels that shouldn't be used for matching into one for easier checking
@@ -301,22 +185,6 @@ const createSilencesSlice = (set, get, options) => ({
// collect all silences
let silences = [...get().silences.items]
- const localItems = get().silences.localItems || {}
-
- // checking if localItem need to overwrite a item or if its appended to silences
- for (const key in localItems) {
- const localSilence = localItems[key]
-
- const index = silences.findIndex((silence) => silence.id === localSilence.id)
-
- if (index !== -1) {
- // Update the existing element
- silences[index] = localSilence
- } else {
- // Add the new element
- silences.unshift(localSilence)
- }
- }
// collect all excluded Labels
const excludedLabels = get().silences.excludedLabels || []
diff --git a/apps/supernova/src/lib/createSilencesSlice.test.jsx b/apps/supernova/src/lib/createSilencesSlice.test.jsx
index 0952ba361..cfdced815 100644
--- a/apps/supernova/src/lib/createSilencesSlice.test.jsx
+++ b/apps/supernova/src/lib/createSilencesSlice.test.jsx
@@ -7,256 +7,20 @@ import * as React from "react"
import { renderHook, act } from "@testing-library/react"
import {
useSilencesActions,
- useSilencesLocalItems,
useAlertsActions,
- useAlertsItems,
useSilencesExcludedLabels,
StoreProvider,
} from "../components/StoreProvider"
-import {
- createFakeAlertStatustWith,
- createFakeAlertWith,
- createFakeSilenceWith,
- createFakeSilenceWithoutAlertFingerprint,
-} from "./fakeObjects"
+import { createFakeAlertStatustWith, createFakeAlertWith, createFakeSilenceWith } from "./fakeObjects"
import { countAlerts } from "./utils"
-describe("addLocalItem", () => {
- it("should append the object with key silence id and value the silence itself", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- actions: useSilencesActions(),
- localSilences: useSilencesLocalItems(),
- }),
- { wrapper }
- )
-
- const silence = createFakeSilenceWith()
-
- act(() => {
- store.result.current.actions.addLocalItem({
- silence: silence,
- id: "test",
- type: "local",
- })
- })
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(1)
- expect(store.result.current.localSilences["test"]["id"]).toEqual("test")
- expect(store.result.current.localSilences["test"].alertFingerprint).toEqual("123")
- })
- it("should avoid to add any silences without id", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- actions: useSilencesActions(),
- localSilences: useSilencesLocalItems(),
- }),
- { wrapper }
- )
-
- const silence = createFakeSilenceWith()
- act(() =>
- store.result.current.actions.addLocalItem({
- silence,
- id: "",
- type: "local",
- })
- )
- act(() =>
- store.result.current.actions.addLocalItem({
- silence,
- id: null,
- type: "local",
- })
- )
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(0)
- })
- it("should add silences with expiring-type and without alert fingerprint. it should delete the silence if a silence with the same id is set in expired state", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- actions: useSilencesActions(),
- localSilences: useSilencesLocalItems(),
- }),
- { wrapper }
- )
-
- const silence = createFakeSilenceWithoutAlertFingerprint({
- status: { state: "expiring" },
- })
- // add a local silence with type expiring
- act(() => {
- store.result.current.actions.addLocalItem({
- silence: silence,
- id: "test",
- type: "expiring",
- })
- })
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(1)
- expect(store.result.current.localSilences["test"]["id"]).toEqual("test")
-
- // set a silence with the same id in expired so it should be deleted (triggers updateLocalItems())
- act(() =>
- store.result.current.actions.setSilences({
- items: [silence],
- itemsHash: { external: silence },
- itemsByState: { expired: [silence] },
- })
- )
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(0)
- })
-
- it("should add items with and expiring type and the local silence should stay if a active silence is set", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- actions: useSilencesActions(),
- localSilences: useSilencesLocalItems(),
- }),
- { wrapper }
- )
-
- const silence = createFakeSilenceWithoutAlertFingerprint({
- status: { state: "expiring" },
- })
-
- act(() => {
- store.result.current.actions.addLocalItem({
- silence: silence,
- id: "test",
- type: "expiring",
- })
- })
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(1)
- expect(store.result.current.localSilences["test"]["id"]).toEqual("test")
- // set a silence with the same id in active so it should not be deleted because
- // local silence is expiring (triggers updateLocalItems())
-
- act(() =>
- store.result.current.actions.setSilences({
- items: [silence],
- itemsHash: { external: silence },
- itemsByState: { active: [silence] },
- })
- )
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(1)
- })
-
- it("should add silences with creating type and delete them if they are set in active silences if they dont have a alertfingerprint", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- actions: useSilencesActions(),
- localSilences: useSilencesLocalItems(),
- }),
- { wrapper }
- )
-
- const silence = createFakeSilenceWithoutAlertFingerprint({
- status: { state: "creating" },
- })
-
- const silence2 = createFakeSilenceWith({
- id: "test2",
- status: { state: "creating" },
- })
- // add a local silences with type creating
- act(() => {
- store.result.current.actions.addLocalItem({
- silence: silence,
- id: "test",
- type: "creating",
- })
-
- store.result.current.actions.addLocalItem({
- silence: silence2,
- id: "test2",
- type: "creating",
- })
- })
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(2)
- expect(store.result.current.localSilences["test"]["id"]).toEqual("test")
- expect(store.result.current.localSilences["test2"]["id"]).toEqual("test2")
-
- // set a silence with the same id in active so it should be deleted (triggers updateLocalItems())
- act(() =>
- store.result.current.actions.setSilences({
- items: [silence, silence2],
- itemsHash: { test: silence, test2: silence2 },
- itemsByState: { active: [silence, silence2] },
- })
- )
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(1)
-
- expect(store.result.current.localSilences["test2"]["id"]).toEqual("test2")
- })
-
- it("should add items with creating type and they should stay if they are set as an expired silence but not if its a pending one", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- actions: useSilencesActions(),
- localSilences: useSilencesLocalItems(),
- }),
- { wrapper }
- )
-
- const silence = createFakeSilenceWithoutAlertFingerprint({
- status: { state: "creating" },
- })
-
- act(() => {
- store.result.current.actions.addLocalItem({
- silence: silence,
- id: "test",
- type: "creating",
- })
- })
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(1)
- expect(store.result.current.localSilences["test"]["id"]).toEqual("test")
- // set a silence with the same id in pending so it should not be deleted because
- // local silence is creating (triggers updateLocalItems())
-
- act(() =>
- store.result.current.actions.setSilences({
- items: [silence],
- itemsHash: { external: silence },
- itemsByState: { expired: [silence] },
- })
- )
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(1)
-
- act(() =>
- store.result.current.actions.setSilences({
- items: [silence],
- itemsHash: { external: silence },
- itemsByState: { pending: [silence] },
- })
- )
-
- expect(Object.keys(store.result.current.localSilences).length).toEqual(0)
- })
-})
-
describe("getMappingSilences", () => {
- it("return all external silences referenced by silencedBy and all local silences with the same fingerprint which are not yet included", () => {
+ it("return all external silences referenced by silencedBy ", () => {
const wrapper = ({ children }) =>
{children}
const store = renderHook(
() => ({
alertActions: useAlertsActions(),
silenceActions: useSilencesActions(),
- localSilences: useSilencesLocalItems(),
}),
{ wrapper }
)
@@ -280,28 +44,14 @@ describe("getMappingSilences", () => {
act(() =>
store.result.current.silenceActions.setSilences({
items: [silence],
- itemsHash: { external: silence },
- itemsByState: { active: [silence] },
- })
- )
- // create local silence adding per attribute the id and the alert fingerprint
- const silence2 = createFakeSilenceWith()
-
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence2,
- id: "local",
- type: "local",
})
)
// get mapping silences
let mappingResult = null
act(() => (mappingResult = store.result.current.silenceActions.getMappingSilences(alert)))
- expect(mappingResult.length).toEqual(2)
+ expect(mappingResult.length).toEqual(1)
expect(mappingResult.map((item) => item.id)).toContainEqual("external")
- expect(mappingResult.map((item) => item.id)).toContainEqual("local")
- expect(mappingResult.find((item) => item.id === "local").type).toEqual("local")
})
it("return silences also when alert silencedBy is just a string", () => {
@@ -324,327 +74,21 @@ describe("getMappingSilences", () => {
counts: countAlerts([alert]),
})
)
- // create local silence
- const silence = createFakeSilenceWith({ id: "external" })
- act(() =>
- store.result.current.silenceActions.setSilences({
- items: [silence],
- itemsHash: { external: silence },
- itemsByState: { active: [silence] },
- })
- )
- // get mapping silences
- let mappingResult = null
- act(() => (mappingResult = store.result.current.silenceActions.getMappingSilences(alert)))
- expect(mappingResult.length).toEqual(1)
- expect(mappingResult.map((item) => item.id)).toContainEqual("external")
- })
-
- it("ignores 'local silences' which are already included in silencedBy and exist as external silence", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- alertActions: useAlertsActions(),
- silenceActions: useSilencesActions(),
- }),
- { wrapper }
- )
-
- // create an alert with custom status
- const status = createFakeAlertStatustWith({
- silencedBy: ["external", "externalAndLocal"],
- })
- const alert = createFakeAlertWith({ status: status, fingerprint: "123" })
- // set the alert
- act(() =>
- store.result.current.alertActions.setAlertsData({
- items: [alert],
- counts: countAlerts([alert]),
- })
- )
- // create external silences adding an id to the object
- const silence = createFakeSilenceWith({ id: "external" })
- const silence2 = createFakeSilenceWith({ id: "externalAndLocal" })
- act(() =>
- store.result.current.silenceActions.setSilences({
- items: [silence, silence2],
- itemsHash: { external: silence, externalAndLocal: silence2 },
- itemsByState: { active: [silence, silence2] },
- })
- )
- // create local silence which already exists as external silence
- const silence3 = createFakeSilenceWith()
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence3,
- id: "externalAndLocal",
- })
- )
- // get mapping silences
- let mappingResult = null
- act(() => (mappingResult = store.result.current.silenceActions.getMappingSilences(alert)))
- expect(mappingResult.length).toEqual(2)
- // checking type to be undefined means that the silence is not local
- expect(mappingResult[0].type).toEqual(undefined)
- expect(mappingResult[1].type).toEqual(undefined)
- })
-
- it("returns local silences when the id exists in silencedBy but it does not exist as external silence", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- alertActions: useAlertsActions(),
- silenceActions: useSilencesActions(),
- }),
- { wrapper }
- )
-
- // create an alert with custom status
- const status = createFakeAlertStatustWith({
- silencedBy: ["external", "local"],
- })
- const alert = createFakeAlertWith({ status: status, fingerprint: "123" })
- // set the alert
- act(() =>
- store.result.current.alertActions.setAlertsData({
- items: [alert],
- counts: countAlerts([alert]),
- })
- )
- // create external silences adding an id to the object
- const silence = createFakeSilenceWith({ id: "external" })
- act(() =>
- store.result.current.silenceActions.setSilences({
- items: [silence],
- itemsHash: { external: silence },
- itemsByState: { active: [silence] },
- })
- )
- // create local silence which already exists as external silence
- const silence2 = createFakeSilenceWith()
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence2,
- id: "local",
- type: "local",
- })
- )
- // get mapping silences
- let mappingResult = null
- act(() => (mappingResult = store.result.current.silenceActions.getMappingSilences(alert)))
- expect(mappingResult.length).toEqual(2)
- // checking type to be undefined means that the silence is not local
- expect(mappingResult[0].type).toEqual(undefined)
- expect(mappingResult[1].type).toEqual("local")
- })
-})
-
-describe("updateLocalItems", () => {
- it("removes local silences whose alert reference (defined by alertFingerprint) has in silencedBy the silence itself and a silence with same id exist also as external silences", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- alertActions: useAlertsActions(),
- silenceActions: useSilencesActions(),
- savedLocalSilences: useSilencesLocalItems(),
- savedAlerts: useAlertsItems(),
- }),
- { wrapper }
- )
-
- // create local silences
- const silence = createFakeSilenceWith({ alertFingerprint: "12345" })
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence,
- id: "test1local",
- type: "local",
- })
- )
- const silence2 = createFakeSilenceWith()
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence2,
- id: "test2local",
- type: "local",
- })
- )
- // check if the local silence are saved
- expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual(2)
- // create an alert without any silencedBy so we just have the local silences
- const status = createFakeAlertStatustWith({
- silencedBy: ["test1local"],
- })
- const alert = createFakeAlertWith({ status: status, fingerprint: "12345" })
- act(() =>
- store.result.current.alertActions.setAlertsData({
- items: [alert],
- counts: countAlerts([alert]),
- })
- )
- // check if the alert is saved
- expect(store.result.current.savedAlerts.length).toEqual(1)
-
- // trigger update local items by setting new external silences
- const externalSilence = createFakeSilenceWith({ id: "test1local" })
- act(() =>
- store.result.current.silenceActions.setSilences({
- items: [externalSilence],
- itemsHash: { [externalSilence.id]: externalSilence },
- itemsByState: { active: [externalSilence] },
- })
- )
- // check local items
- expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual(1)
- expect(store.result.current.savedLocalSilences["test2local"].id).toEqual("test2local")
- })
-
- it("keeps local silences if silence with same id does not exist yet in external silences", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- alertActions: useAlertsActions(),
- silenceActions: useSilencesActions(),
- savedLocalSilences: useSilencesLocalItems(),
- savedAlerts: useAlertsItems(),
- }),
- { wrapper }
- )
-
- // create local silences
- const silence = createFakeSilenceWith()
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence,
- id: "test1local",
- type: "local",
- })
- )
- const silence2 = createFakeSilenceWith()
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence2,
- id: "test2local",
- type: "local",
- })
- )
- // check if the local silence are saved
- expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual(2)
- // create an alert without any silencedBy so we just have the local silences
- const status = createFakeAlertStatustWith({
- silencedBy: ["test1local"],
- })
- const alert = createFakeAlertWith({ status: status, fingerprint: "12345" })
- act(() =>
- store.result.current.alertActions.setAlertsData({
- items: [alert],
- counts: countAlerts([alert]),
- })
- )
- // check if the alert is saved
- expect(store.result.current.savedAlerts.length).toEqual(1)
- // trigger update local items by setting new external silences
- const externalSilence = createFakeSilenceWith({
- id: "different_id_then_test1local",
- })
- act(() =>
- store.result.current.silenceActions.setSilences({
- items: [externalSilence],
- itemsHash: { [externalSilence.id]: externalSilence },
- itemsByState: { active: [externalSilence] },
- })
- )
- // check local items
- expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual(2)
- expect(store.result.current.savedLocalSilences["test1local"].id).toEqual("test1local")
- expect(store.result.current.savedLocalSilences["test2local"].id).toEqual("test2local")
- })
-})
-
-describe("getMappedState", () => {
- it("retuns supressed (processing) if a local silence is found", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- alertActions: useAlertsActions(),
- silenceActions: useSilencesActions(),
- }),
- { wrapper }
- )
- // create an alert with custom status
- const status = createFakeAlertStatustWith({
- silencedBy: ["external"],
- })
- const alert = createFakeAlertWith({ status: status, fingerprint: "123" })
- // set the alert
- act(() =>
- store.result.current.alertActions.setAlertsData({
- items: [alert],
- counts: countAlerts([alert]),
- })
- )
// create extern silences adding an id to the object
const silence = createFakeSilenceWith({ id: "external" })
- act(() =>
- store.result.current.silenceActions.setSilences({
- items: [silence],
- itemsHash: { external: silence },
- itemsByState: { active: [silence] },
- })
- )
- // create local silence adding per attribute the id and the alert fingerprint
- const silence2 = createFakeSilenceWith()
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence2,
- id: "local",
- type: "local",
- })
- )
- // get mapping silences
- let mappingResult = null
- act(() => (mappingResult = store.result.current.silenceActions.getMappedState(alert)))
- expect(mappingResult["type"]).toEqual("suppressed")
- expect(mappingResult["isProcessing"]).toEqual(true)
- })
-
- it("retuns just the alert.status.state if no local silences found", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- alertActions: useAlertsActions(),
- silenceActions: useSilencesActions(),
- }),
- { wrapper }
- )
- // create an alert with custom status
- const status = createFakeAlertStatustWith({
- silencedBy: ["external"],
- })
- const alert = createFakeAlertWith({ status: status, fingerprint: "123" })
- // set the alert
- act(() =>
- store.result.current.alertActions.setAlertsData({
- items: [alert],
- counts: countAlerts([alert]),
- })
- )
- // create extern silences adding an id to the object
- const silence = createFakeSilenceWith({ id: "external" })
act(() =>
store.result.current.silenceActions.setSilences({
items: [silence],
- itemsHash: { external: silence },
- itemsByState: { active: [silence] },
})
)
+
// get mapping silences
let mappingResult = null
- act(() => (mappingResult = store.result.current.silenceActions.getMappedState(alert)))
- expect(mappingResult["type"]).toEqual(alert?.status?.state)
- expect(mappingResult["isProcessing"]).toEqual(false)
+ act(() => (mappingResult = store.result.current.silenceActions.getMappingSilences(alert)))
+ expect(mappingResult.length).toEqual(1)
+ expect(mappingResult.map((item) => item.id)).toContainEqual("external")
})
})
@@ -719,8 +163,6 @@ describe("getExpiredSilences", () => {
act(() =>
store.result.current.silenceActions.setSilences({
items: [silence, silence2, silence3],
- itemsHash: { test1: silence, test2: silence2, test3: silence3 },
- itemsByState: { expired: [silence, silence2, silence3] },
})
)
// get mapping silences
@@ -732,62 +174,7 @@ describe("getExpiredSilences", () => {
})
describe("getLatestMappingSilence", () => {
- it("returns the silence with the latest endsAt timestamp when local", () => {
- const wrapper = ({ children }) =>
{children}
- const store = renderHook(
- () => ({
- alertActions: useAlertsActions(),
- silenceActions: useSilencesActions(),
- }),
- { wrapper }
- )
-
- // create an alert with custom status
- const status = createFakeAlertStatustWith({
- silencedBy: ["external"],
- })
- const alert = createFakeAlertWith({ status: status, fingerprint: "123" })
- // set the alert
- act(() =>
- store.result.current.alertActions.setAlertsData({
- items: [alert],
- counts: countAlerts([alert]),
- })
- )
- // create extern silences adding an id to the object
- const silence = createFakeSilenceWith({
- id: "external",
- endsAt: "2023-06-21T15:17:28.327Z",
- })
- const silence2 = createFakeSilenceWith({
- id: "external2",
- endsAt: "2023-06-21T16:18:28.327Z",
- })
- act(() =>
- store.result.current.silenceActions.setSilences({
- items: [silence, silence2],
- itemsHash: { external: silence, external2: silence2 },
- itemsByState: { active: [silence, silence2] },
- })
- )
- // create local silence adding per attribute the id and the alert fingerprint
- const silence3 = createFakeSilenceWith({
- endsAt: "2023-06-21T19:17:28.327Z",
- })
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence3,
- id: "local",
- type: "local",
- })
- )
- // get mapping silences
- let mappingResult = null
- act(() => (mappingResult = store.result.current.silenceActions.getLatestMappingSilence(alert)))
- expect(mappingResult.id).toEqual("local")
- })
-
- it("returns the silence with the latest endsAt timestamp when external", () => {
+ it("returns the silence with the latest endsAt timestamp ", () => {
const wrapper = ({ children }) =>
{children}
const store = renderHook(
() => ({
@@ -821,21 +208,9 @@ describe("getLatestMappingSilence", () => {
act(() =>
store.result.current.silenceActions.setSilences({
items: [silence, silence2],
- itemsHash: { external: silence, external2: silence2 },
- itemsByState: { active: [silence, silence2] },
- })
- )
- // create local silence adding per attribute the id and the alert fingerprint
- const silence3 = createFakeSilenceWith({
- endsAt: "2023-06-21T19:17:28.327Z",
- })
- act(() =>
- store.result.current.silenceActions.addLocalItem({
- silence: silence3,
- id: "local",
- type: "local",
})
)
+
// get mapping silences
let mappingResult = null
act(() => (mappingResult = store.result.current.silenceActions.getLatestMappingSilence(alert)))
diff --git a/apps/supernova/src/lib/queries/silencesQueries.js b/apps/supernova/src/lib/queries/silencesQueries.js
deleted file mode 100644
index 62eb48868..000000000
--- a/apps/supernova/src/lib/queries/silencesQueries.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { sortSilencesByState } from "../utils"
-
-export const fetchSilences = async (endpoint) => {
- try {
- const response = await fetch(`${endpoint}/silences`)
-
- if (!response.ok) {
- // Parse the error object from the response body
- const errorObject = await response.json().catch(() => {
- throw new Error(`Unexpected error: Unable to parse error response.`)
- })
-
- // Throw the error object directly
- throw errorObject
- }
-
- const items = await response.json() // Parse JSON data
-
- // Convert items to hash for easier access
- const itemsHash = items.reduce((hash, silence) => {
- hash[silence.id] = silence
- return hash
- }, {})
-
- // Split items by state (active, pending, expired)
- const itemsByState = sortSilencesByState(items)
-
- // Return the structured result
- return {
- silences: items,
- silencesHash: itemsHash,
- silencesByState: itemsByState,
- }
- } catch (error) {
- console.error(error)
- throw error // Let React Query handle the error
- }
-}
diff --git a/apps/supernova/src/lib/utils.js b/apps/supernova/src/lib/utils.js
index e33bdf5f9..ab0eecc7b 100644
--- a/apps/supernova/src/lib/utils.js
+++ b/apps/supernova/src/lib/utils.js
@@ -49,23 +49,6 @@ export const humanizeString = (value) => {
return humanized
}
-// sort silences by state
-// {
-// active: [...], pending: [...], expired:[...], ...
-// }
-export const sortSilencesByState = (silences) => {
- const sortedSilences = {}
-
- if (!silences || silences.length === 0) return {}
-
- silences.forEach((silences) => {
- const state = silences.status?.state
- if (!sortedSilences[state]) sortedSilences[state] = [] // init
- sortedSilences[state].push(silences)
- })
- return sortedSilences
-}
-
// count alerts and create a map
// {
// global: { total: number, critical: number, ...},