Files
homarr/src/pages/unraid/settings/notifications.tsx
Kaloyan Danchev 9a2c56a5dc
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
Phase 4: Add Unraid management pages with sidebar layout
- Add UnraidLayout component with full sidebar navigation
- Add Array management page with disk tables and parity check controls
- Add Docker management page with container cards and filtering
- Add VMs management page with power controls (start/stop/pause/resume/reboot)
- Add Shares page with security levels and storage usage
- Add Users page with admin/user roles display
- Add Settings index with links to all settings pages
- Add Identification settings page with system info
- Add Notifications settings page with notification history
- Add Tools index with links to all tools
- Add Syslog page with live log viewing and filtering
- Add Diagnostics page with system health checks
- Update dashboard to use UnraidLayout

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 09:32:52 +02:00

460 lines
13 KiB
TypeScript

/**
* Notifications Settings Page
* Configure notification preferences and view notification history
*/
import { useState } from 'react';
import {
ActionIcon,
Alert,
Badge,
Button,
Card,
Container,
Divider,
Group,
Loader,
Menu,
Paper,
ScrollArea,
Stack,
Switch,
Text,
ThemeIcon,
Title,
Tooltip,
} from '@mantine/core';
import { notifications as mantineNotifications } from '@mantine/notifications';
import {
IconBell,
IconBellOff,
IconAlertCircle,
IconCheck,
IconTrash,
IconDots,
IconRefresh,
IconMail,
IconBrandSlack,
IconBrandDiscord,
IconAlertTriangle,
IconInfoCircle,
} from '@tabler/icons-react';
import { GetServerSidePropsContext } from 'next';
import { UnraidLayout } from '~/components/Unraid/Layout';
import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { api } from '~/utils/api';
import type { Notification, NotificationImportance } from '~/lib/unraid/types';
function getImportanceColor(importance: NotificationImportance): string {
switch (importance) {
case 'ALERT':
return 'red';
case 'WARNING':
return 'yellow';
case 'NORMAL':
return 'blue';
default:
return 'gray';
}
}
function getImportanceIcon(importance: NotificationImportance) {
switch (importance) {
case 'ALERT':
return <IconAlertCircle size={14} />;
case 'WARNING':
return <IconAlertTriangle size={14} />;
case 'NORMAL':
return <IconInfoCircle size={14} />;
default:
return <IconBell size={14} />;
}
}
function formatDate(timestamp: number): string {
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
function NotificationItem({
notification,
onMarkRead,
onDelete,
}: {
notification: Notification;
onMarkRead: () => void;
onDelete: () => void;
}) {
return (
<Paper
p="md"
radius="md"
withBorder
style={{
opacity: notification.read ? 0.7 : 1,
borderLeftWidth: 3,
borderLeftColor: `var(--mantine-color-${getImportanceColor(notification.importance)}-6)`,
}}
>
<Group position="apart" noWrap>
<Group spacing="sm" noWrap style={{ flex: 1 }}>
<ThemeIcon
size="md"
variant="light"
color={getImportanceColor(notification.importance)}
>
{getImportanceIcon(notification.importance)}
</ThemeIcon>
<div style={{ flex: 1, minWidth: 0 }}>
<Group spacing="xs" noWrap>
<Text weight={600} lineClamp={1}>
{notification.subject}
</Text>
{!notification.read && (
<Badge size="xs" color="blue" variant="filled">
New
</Badge>
)}
</Group>
<Text size="sm" color="dimmed" lineClamp={2}>
{notification.description}
</Text>
<Group spacing="xs" mt="xs">
<Badge size="xs" variant="outline">
{notification.type}
</Badge>
<Text size="xs" color="dimmed">
{formatDate(notification.timestamp)}
</Text>
</Group>
</div>
</Group>
<Menu shadow="md" width={150} position="bottom-end">
<Menu.Target>
<ActionIcon variant="subtle">
<IconDots size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{!notification.read && (
<Menu.Item
icon={<IconCheck size={14} />}
onClick={onMarkRead}
>
Mark as Read
</Menu.Item>
)}
<Menu.Item
color="red"
icon={<IconTrash size={14} />}
onClick={onDelete}
>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Paper>
);
}
export default function NotificationsSettingsPage() {
const [filter, setFilter] = useState<'all' | 'unread'>('all');
const {
data: notificationsList,
isLoading,
error,
refetch,
} = api.unraid.notifications.useQuery();
const markRead = api.unraid.markNotificationRead.useMutation({
onSuccess: () => {
mantineNotifications.show({
title: 'Marked as Read',
message: 'Notification marked as read',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
});
const deleteNotification = api.unraid.deleteNotification.useMutation({
onSuccess: () => {
mantineNotifications.show({
title: 'Deleted',
message: 'Notification deleted',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
});
const markAllRead = api.unraid.markAllNotificationsRead.useMutation({
onSuccess: () => {
mantineNotifications.show({
title: 'All Read',
message: 'All notifications marked as read',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
});
const filteredNotifications = notificationsList?.filter((n) =>
filter === 'all' ? true : !n.read
);
const unreadCount = notificationsList?.filter((n) => !n.read).length || 0;
const alertCount = notificationsList?.filter((n) => n.importance === 'ALERT').length || 0;
const warningCount = notificationsList?.filter((n) => n.importance === 'WARNING').length || 0;
if (isLoading) {
return (
<UnraidLayout>
<Container size="lg" py="xl">
<Stack align="center" spacing="md">
<Loader size="xl" />
<Text color="dimmed">Loading notifications...</Text>
</Stack>
</Container>
</UnraidLayout>
);
}
if (error) {
return (
<UnraidLayout>
<Container size="lg" py="xl">
<Alert icon={<IconAlertCircle size={16} />} title="Error" color="red">
{error.message}
</Alert>
</Container>
</UnraidLayout>
);
}
return (
<UnraidLayout>
<Container size="lg" py="xl">
<Stack spacing="xl">
{/* Header */}
<Group position="apart">
<Group>
<ThemeIcon size={48} radius="md" variant="light" color="yellow">
<IconBell size={28} />
</ThemeIcon>
<div>
<Title order={2}>Notifications</Title>
<Text color="dimmed" size="sm">
{unreadCount} unread notification{unreadCount !== 1 ? 's' : ''}
</Text>
</div>
</Group>
<Group>
{unreadCount > 0 && (
<Button
variant="light"
leftIcon={<IconCheck size={16} />}
onClick={() => markAllRead.mutate()}
loading={markAllRead.isLoading}
>
Mark All Read
</Button>
)}
<Button
variant="light"
leftIcon={<IconRefresh size={16} />}
onClick={() => refetch()}
>
Refresh
</Button>
</Group>
</Group>
{/* Stats */}
<Group>
<Card shadow="sm" radius="md" withBorder style={{ flex: 1 }}>
<Text size="xs" color="dimmed" transform="uppercase" weight={600}>
Unread
</Text>
<Text size="xl" weight={700} color="blue">
{unreadCount}
</Text>
</Card>
<Card shadow="sm" radius="md" withBorder style={{ flex: 1 }}>
<Text size="xs" color="dimmed" transform="uppercase" weight={600}>
Alerts
</Text>
<Text size="xl" weight={700} color="red">
{alertCount}
</Text>
</Card>
<Card shadow="sm" radius="md" withBorder style={{ flex: 1 }}>
<Text size="xs" color="dimmed" transform="uppercase" weight={600}>
Warnings
</Text>
<Text size="xl" weight={700} color="yellow">
{warningCount}
</Text>
</Card>
<Card shadow="sm" radius="md" withBorder style={{ flex: 1 }}>
<Text size="xs" color="dimmed" transform="uppercase" weight={600}>
Total
</Text>
<Text size="xl" weight={700}>
{notificationsList?.length || 0}
</Text>
</Card>
</Group>
{/* Notification Settings (placeholder) */}
<Card shadow="sm" radius="md" withBorder>
<Title order={4} mb="md">
Notification Channels
</Title>
<Stack spacing="md">
<Group position="apart">
<Group spacing="sm">
<ThemeIcon size="md" variant="light" color="blue">
<IconMail size={16} />
</ThemeIcon>
<div>
<Text weight={500}>Email Notifications</Text>
<Text size="xs" color="dimmed">
Send notifications via email
</Text>
</div>
</Group>
<Switch disabled />
</Group>
<Divider />
<Group position="apart">
<Group spacing="sm">
<ThemeIcon size="md" variant="light" color="grape">
<IconBrandSlack size={16} />
</ThemeIcon>
<div>
<Text weight={500}>Slack Notifications</Text>
<Text size="xs" color="dimmed">
Send notifications to Slack
</Text>
</div>
</Group>
<Switch disabled />
</Group>
<Divider />
<Group position="apart">
<Group spacing="sm">
<ThemeIcon size="md" variant="light" color="indigo">
<IconBrandDiscord size={16} />
</ThemeIcon>
<div>
<Text weight={500}>Discord Notifications</Text>
<Text size="xs" color="dimmed">
Send notifications to Discord
</Text>
</div>
</Group>
<Switch disabled />
</Group>
</Stack>
</Card>
{/* Filter */}
<Group>
<Button
variant={filter === 'all' ? 'filled' : 'light'}
onClick={() => setFilter('all')}
size="sm"
>
All ({notificationsList?.length || 0})
</Button>
<Button
variant={filter === 'unread' ? 'filled' : 'light'}
onClick={() => setFilter('unread')}
size="sm"
color="blue"
>
Unread ({unreadCount})
</Button>
</Group>
{/* Notifications List */}
<Stack spacing="sm">
{filteredNotifications?.map((notification) => (
<NotificationItem
key={notification.id}
notification={notification}
onMarkRead={() => markRead.mutate({ id: notification.id })}
onDelete={() => deleteNotification.mutate({ id: notification.id })}
/>
))}
{filteredNotifications?.length === 0 && (
<Card shadow="sm" radius="md" withBorder p="xl">
<Stack align="center" spacing="md">
<ThemeIcon size={48} variant="light" color="gray">
<IconBellOff size={24} />
</ThemeIcon>
<Text color="dimmed">
{filter === 'unread'
? 'No unread notifications'
: 'No notifications'}
</Text>
</Stack>
</Card>
)}
</Stack>
</Stack>
</Container>
</UnraidLayout>
);
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerAuthSession(context);
const translations = await getServerSideTranslations(
['common'],
context.locale,
context.req,
context.res
);
const result = checkForSessionOrAskForLogin(
context,
session,
() => session?.user != undefined
);
if (result) {
return result;
}
return {
props: {
...translations,
},
};
};