From e51ec6a827418b2b4282e20d244ae8f79b244c1f Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Tue, 21 Oct 2025 14:56:20 +0200 Subject: [PATCH] Dashboards: Hide error notifications in kiosk mode on dashboards (#112390) Hide error notifications in kiosk mode on dashboards Suppress error alerts when dashboard is viewed in kiosk mode Kiosk mode is typically used for TV displays without interaction Other notification types (success, warning, info) remain visible --------- Co-authored-by: Tom Ratcliffe --- .../AppNotificationList.test.tsx | 123 ++++++++++++++++++ .../AppNotifications/AppNotificationList.tsx | 23 +++- 2 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 public/app/core/components/AppNotifications/AppNotificationList.test.tsx diff --git a/public/app/core/components/AppNotifications/AppNotificationList.test.tsx b/public/app/core/components/AppNotifications/AppNotificationList.test.tsx new file mode 100644 index 00000000000..c310a6c7f8d --- /dev/null +++ b/public/app/core/components/AppNotifications/AppNotificationList.test.tsx @@ -0,0 +1,123 @@ +import { act, getWrapper, render, screen } from 'test/test-utils'; + +import { AppEvents } from '@grafana/data'; +import appEvents from 'app/core/app_events'; +import { KioskMode } from 'app/types/dashboard'; + +import { AppChromeService } from '../AppChrome/AppChromeService'; + +import { AppNotificationList } from './AppNotificationList'; + +const renderWithContext = (kioskMode?: KioskMode, pathname = '/') => { + const chromeService = new AppChromeService(); + if (kioskMode) { + chromeService.update({ kioskMode }); + } + + const wrapper = getWrapper({ + renderWithRouter: true, + historyOptions: { initialEntries: [pathname] }, + grafanaContext: { + chrome: chromeService, + }, + }); + const view = render(, { wrapper }); + + return view; +}; + +const expectedErrorMessage = 'Test error'; +const expectedSuccessMessage = 'Test success'; +const expectedWarningMessage = 'Test warning'; +const expectedInfoMessage = 'Test info'; + +const sendTestNotification = async (type: (typeof AppEvents)[keyof typeof AppEvents], message: string) => { + return act(async () => { + appEvents.publish({ type: type.name, payload: [message] }); + }); +}; + +describe('AppNotificationList', () => { + describe('Error notifications', () => { + it('should show error notifications when not in kiosk mode', async () => { + renderWithContext(undefined, '/d/test-dashboard'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(await screen.findByText(expectedErrorMessage)).toBeInTheDocument(); + }); + + it('should hide error notifications in kiosk mode on dashboard page', async () => { + renderWithContext(KioskMode.Full, '/d/test-dashboard'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(screen.queryByText(expectedErrorMessage)).not.toBeInTheDocument(); + }); + + it('should show error notifications in kiosk mode on non-dashboard pages', async () => { + renderWithContext(KioskMode.Full, '/alerting'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(await screen.findByText(expectedErrorMessage)).toBeInTheDocument(); + }); + + it('should hide error notifications in kiosk mode on home dashboard', async () => { + renderWithContext(KioskMode.Full, '/d/'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(screen.queryByText(expectedErrorMessage)).not.toBeInTheDocument(); + }); + + it('should show error notifications in kiosk mode on root page (not dashboard route)', async () => { + renderWithContext(KioskMode.Full, '/'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(await screen.findByText(expectedErrorMessage)).toBeInTheDocument(); + }); + }); + + describe('Other notification types', () => { + it('should always show success notifications in kiosk mode on dashboard', async () => { + renderWithContext(KioskMode.Full, '/d/test-dashboard'); + await sendTestNotification(AppEvents.alertSuccess, expectedSuccessMessage); + + expect(await screen.findByText(expectedSuccessMessage)).toBeInTheDocument(); + }); + + it('should always show warning notifications in kiosk mode on dashboard', async () => { + renderWithContext(KioskMode.Full, '/d/test-dashboard'); + await sendTestNotification(AppEvents.alertWarning, expectedWarningMessage); + + expect(await screen.findByText(expectedWarningMessage)).toBeInTheDocument(); + }); + + it('should always show info notifications in kiosk mode on dashboard', async () => { + renderWithContext(KioskMode.Full, '/d/test-dashboard'); + await sendTestNotification(AppEvents.alertInfo, expectedInfoMessage); + + expect(await screen.findByText(expectedInfoMessage)).toBeInTheDocument(); + }); + }); + + describe('Edge cases', () => { + it('should show error on dashboard page with uid and slug', async () => { + renderWithContext(undefined, '/d/test-uid/test-slug'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(await screen.findByText(expectedErrorMessage)).toBeInTheDocument(); + }); + + it('should hide error in kiosk mode on dashboard page with uid and slug', async () => { + renderWithContext(KioskMode.Full, '/d/test-uid/test-slug'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(screen.queryByText(expectedErrorMessage)).not.toBeInTheDocument(); + }); + + it('should show error on legacy dashboard route', async () => { + renderWithContext(KioskMode.Full, '/dashboard/db/test-dashboard'); + await sendTestNotification(AppEvents.alertError, expectedErrorMessage); + + expect(await screen.findByText(expectedErrorMessage)).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index bb473dd0905..737e1f7cbf7 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -1,10 +1,12 @@ import { css } from '@emotion/css'; import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; -import { AppEvents, GrafanaTheme2 } from '@grafana/data'; +import { AlertErrorPayload, AppEvents, GrafanaTheme2 } from '@grafana/data'; import { useStyles2, Stack } from '@grafana/ui'; import { notifyApp, hideAppNotification } from 'app/core/actions'; import appEvents from 'app/core/app_events'; +import { useGrafana } from 'app/core/context/GrafanaContext'; import { selectVisible } from 'app/core/reducers/appNotification'; import { useSelector, useDispatch } from 'app/types/store'; @@ -21,13 +23,28 @@ export function AppNotificationList() { const appNotifications = useSelector((state) => selectVisible(state.appNotifications)); const dispatch = useDispatch(); const styles = useStyles2(getStyles); + const { chrome } = useGrafana(); + const location = useLocation(); useEffect(() => { + // Suppress error notifications in kiosk mode on dashboards. + // Kiosk mode is typically used for TV displays which are non-interactive. + // Backend errors like "Failed to fetch" cannot be dismissed and would remain visible, + // degrading the viewing experience. Other notification types (success, warning, info) + // are still shown as they indicate successful operations or important information. + const handleErrorAlert = (payload: AlertErrorPayload) => { + const isKioskDashboard = chrome.state.getValue().kioskMode && location.pathname.startsWith('/d/'); + + if (!isKioskDashboard) { + dispatch(notifyApp(createErrorNotification(...payload))); + } + }; + appEvents.on(AppEvents.alertWarning, (payload) => dispatch(notifyApp(createWarningNotification(...payload)))); appEvents.on(AppEvents.alertSuccess, (payload) => dispatch(notifyApp(createSuccessNotification(...payload)))); - appEvents.on(AppEvents.alertError, (payload) => dispatch(notifyApp(createErrorNotification(...payload)))); + appEvents.on(AppEvents.alertError, handleErrorAlert); appEvents.on(AppEvents.alertInfo, (payload) => dispatch(notifyApp(createInfoNotification(...payload)))); - }, [dispatch]); + }, [dispatch, chrome, location.pathname]); const onClearAppNotification = (id: string) => { dispatch(hideAppNotification(id));