Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
- 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>
460 lines
13 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
};
|