Phase 4: Add Unraid management pages with sidebar layout
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
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>
This commit is contained in:
459
src/pages/unraid/settings/notifications.tsx
Normal file
459
src/pages/unraid/settings/notifications.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user