fix: critical alerts modal mute toggles no longer close popover or fail to save (#9969)

* fix: mute toggles in critical alerts modal no longer close popover or fail to save

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: mark popover content root as dropdown-portal so padding clicks don't close modal

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: derive no-channels warning from mute state so it survives modal reopen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-07-06 20:43:04 +02:00
committed by GitHub
parent 45946d1185
commit 6587019d26
2 changed files with 40 additions and 36 deletions
@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { onMount, onDestroy } from 'svelte'
import CriticalAlertModalInner from './CriticalAlertModalInner.svelte'
import { SettingService, type CriticalAlert } from '$lib/gen'
@@ -35,26 +33,11 @@
let workspaceContext = $state(false)
let childRef: CriticalAlertModalInner | undefined = $state()
function setupApiFunctions(_ctx?) {
getCriticalAlerts = withSuperadminLogic(
SettingService.getCriticalAlerts,
SettingService.workspaceGetCriticalAlerts
)
acknowledgeCriticalAlert = withSuperadminLogic(
SettingService.acknowledgeCriticalAlert,
SettingService.workspaceAcknowledgeCriticalAlert
)
acknowledgeAllCriticalAlerts = withSuperadminLogic(
SettingService.acknowledgeAllCriticalAlerts,
SettingService.workspaceAcknowledgeAllCriticalAlerts
)
}
let checkForNewAlertsInterval: ReturnType<typeof setInterval>
let checkingForNewAlerts = false
// The returned closure reads workspaceContext / $workspaceStore / $devopsRole at
// call time, so the wrappers are stable and never need recreating.
const withSuperadminLogic = (superadminFunction, workspaceFunction) => {
return async (params = {}) => {
if (!$devopsRole || workspaceContext) {
@@ -68,12 +51,21 @@
}
}
type AckFn = (params?: {}) => Promise<any>
let getCriticalAlerts: AckFn | undefined = $state()
let acknowledgeCriticalAlert: AckFn | undefined = $state()
let acknowledgeAllCriticalAlerts: AckFn | undefined = $state()
setupApiFunctions()
let getCriticalAlerts = $derived(
withSuperadminLogic(SettingService.getCriticalAlerts, SettingService.workspaceGetCriticalAlerts)
)
let acknowledgeCriticalAlert = $derived(
withSuperadminLogic(
SettingService.acknowledgeCriticalAlert,
SettingService.workspaceAcknowledgeCriticalAlert
)
)
let acknowledgeAllCriticalAlerts = $derived(
withSuperadminLogic(
SettingService.acknowledgeAllCriticalAlerts,
SettingService.workspaceAcknowledgeAllCriticalAlerts
)
)
onMount(async () => {
await updateHasUnacknowledgedCriticalAlerts(false)
@@ -173,13 +165,10 @@
await acknowledgeCriticalAlert?.({ id })
updateHasUnacknowledgedCriticalAlerts()
}
run(() => {
setupApiFunctions(workspaceContext)
})
run(() => {
$effect(() => {
if ($isCriticalAlertsUIOpen) open = $isCriticalAlertsUIOpen
})
run(() => {
$effect(() => {
isCriticalAlertsUIOpen.set(open)
})
</script>
@@ -197,10 +186,14 @@
{#snippet headerRight()}
<List horizontal>
{#if $superadmin || $userStore?.is_admin}
<!-- Portal to `body` (not the trigger) so toggle clicks don't bubble to the melt
trigger and toggle the popover shut. `dropdown-portal` on the content root is a
`portalDivs` marker, so the enclosing Modal2's clickOutside treats the whole
popover surface — padding included — as inside a portal and stays open. -->
<Popover
floatingConfig={{ strategy: 'fixed', placement: 'bottom-end' }}
portal="#mute-settings-button"
contentClasses="p-4"
portal="body"
contentClasses="p-4 dropdown-portal"
>
{#snippet trigger()}
<div id="mute-settings-button">
@@ -246,8 +239,8 @@
{#if $superadmin}
<Popover
floatingConfig={{ strategy: 'fixed', placement: 'bottom-end' }}
portal="#settings-button"
contentClasses="p-4"
portal="body"
contentClasses="p-4 dropdown-portal"
>
{#snippet trigger()}
<div id="settings-button">
@@ -304,6 +297,7 @@
<CriticalAlertModalInner
bind:workspaceContext
{muteSettings}
{numUnacknowledgedCriticalAlerts}
{updateHasUnacknowledgedCriticalAlerts}
{getCriticalAlerts}
@@ -11,7 +11,7 @@
import CriticalAlertTable from './CriticalAlertTable.svelte'
import Alert from '$lib/components/common/alert/Alert.svelte'
import { sendUserToast } from '$lib/toast'
import { untrack } from 'svelte'
import { onMount, untrack } from 'svelte'
let filteredAlerts: CriticalAlert[] = $state([])
@@ -30,6 +30,7 @@
acknowledgeCriticalAlert: any
acknowledgeAllCriticalAlerts: any
numUnacknowledgedCriticalAlerts: any
muteSettings?: { global?: boolean; workspace?: boolean }
workspaceContext?: boolean
}
@@ -39,9 +40,12 @@
acknowledgeCriticalAlert,
acknowledgeAllCriticalAlerts,
numUnacknowledgedCriticalAlerts,
muteSettings,
workspaceContext = $bindable(false)
}: Props = $props()
let isMuted = $derived(Boolean(muteSettings?.global || muteSettings?.workspace))
async function acknowledgeAll() {
await acknowledgeAllCriticalAlerts()
getAlerts(false)
@@ -82,6 +86,12 @@
hasCriticalAlertChannels = channels && channels.length > 0
}
// Load the channel state on mount so the "no channels" warning doesn't depend on
// there being unacknowledged alerts to trigger a refresh (muting auto-acks them).
onMount(() => {
if ($superadmin) checkCriticalAlertChannels()
})
async function acknowledgeAlert(id: number) {
await acknowledgeCriticalAlert({ id })
getAlerts(false)
@@ -133,7 +143,7 @@
</script>
<List gap="sm">
{#if !hasCriticalAlertChannels && $superadmin}
{#if $superadmin && isMuted && !hasCriticalAlertChannels}
<div class="w-full">
<Alert title="No critical alert channels are set up" type="warning" size="xs">
Go to the