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));