Files
checkcle/application/src/components/settings/notification-settings/NotificationChannelList.tsx
T
MrSchneemann 91ceddc489 feat: add Matrix chat notification support (#211)
Implements native Matrix notification channel support, allowing users
to receive service alerts in any Matrix room via the Client-Server API.

Changes:
- server/service-operation/notification/matrix.go: new Matrix service
  implementing the NotificationService interface (PUT /_matrix/client/v3/...)
- server/service-operation/notification/types.go: add MatrixHomeserver,
  MatrixRoomID, MatrixAccessToken fields to AlertConfiguration
- server/service-operation/notification/manager.go: register MatrixService
- server/pb_migrations/1772200000_updated_alert_configurations_matrix.js:
  PocketBase migration adding matrix fields and notification_type value
- application: Matrix option in channel dialog with homeserver/room/token
  fields, tab filter, list display, type definitions and translations (en, km)
- application/public/upload/notification/matrix.png: Matrix channel icon
2026-03-01 10:48:01 +01:00

192 lines
6.1 KiB
TypeScript

import { AlertConfiguration } from "@/services/alertConfigService";
import { Bell, Edit, Trash2 } from "lucide-react";
import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { alertConfigService } from "@/services/alertConfigService";
import { pb } from "@/lib/pocketbase";
interface CombinedChannel extends Partial<AlertConfiguration> {
isWebhook?: boolean;
url?: string;
method?: string;
description?: string;
}
interface NotificationChannelListProps {
channels: CombinedChannel[];
onEdit: (config: AlertConfiguration) => void;
onDelete: (id: string) => void;
}
export const NotificationChannelList = ({
channels,
onEdit,
onDelete
}: NotificationChannelListProps) => {
const [optimisticStates, setOptimisticStates] = useState<Record<string, boolean>>({});
const toggleEnabled = async (config: CombinedChannel) => {
if (!config.id) return;
const currentEnabled = typeof config.enabled === 'string'
? config.enabled === "true" || config.enabled === "on"
: !!config.enabled;
// Apply optimistic update immediately
setOptimisticStates(prev => ({
...prev,
[config.id!]: !currentEnabled
}));
try {
if (config.isWebhook) {
// Handle webhook toggle
const newEnabled = currentEnabled ? "off" : "on";
await pb.collection('webhook').update(config.id, {
enabled: newEnabled
});
} else {
// Handle alert config toggle
await alertConfigService.updateAlertConfiguration(config.id, {
enabled: !currentEnabled
});
}
} catch (error) {
setOptimisticStates(prev => ({
...prev,
[config.id!]: currentEnabled
}));
}
};
const getChannelTypeLabel = (type: string | undefined) => {
switch(type) {
case "telegram": return "Telegram";
case "discord": return "Discord";
case "slack": return "Slack";
case "signal": return "Signal";
case "google_chat": return "Google Chat";
case "email": return "Email";
case "pushover": return "Pushover";
case "notifiarr": return "Notifiarr";
case "webhook": return "Webhook";
case "matrix": return "Matrix";
default: return type || "Unknown";
}
};
const getChannelDetails = (config: CombinedChannel) => {
if (config.isWebhook) {
return `${config.method || 'POST'} ${config.url || ''}`;
}
switch(config.notification_type) {
case "telegram":
return config.telegram_chat_id || '';
case "discord":
case "slack":
case "google_chat":
return config.discord_webhook_url || config.slack_webhook_url || config.google_chat_webhook_url || '';
case "signal":
return config.signal_number || '';
case "email":
return config.email_address || '';
case "matrix":
return config.matrix_room_id || '';
default:
return '';
}
};
if (channels.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-10 text-center">
<Bell className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium">No notification channels configured</h3>
<p className="text-muted-foreground mt-2">
Add a notification channel to get alerts when your services go down.
</p>
</div>
);
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Details</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channels.map((channel) => (
<TableRow key={channel.id}>
<TableCell className="font-medium">{channel.notify_name}</TableCell>
<TableCell>
<Badge variant="outline">{getChannelTypeLabel(channel.notification_type)}</Badge>
</TableCell>
<TableCell className="max-w-xs truncate text-sm text-muted-foreground">
{getChannelDetails(channel)}
</TableCell>
<TableCell>
<Switch
checked={
channel.id && optimisticStates.hasOwnProperty(channel.id)
? optimisticStates[channel.id]
: typeof channel.enabled === 'string'
? channel.enabled === "true" || channel.enabled === "on"
: !!channel.enabled
}
onCheckedChange={() => toggleEnabled(channel)}
/>
</TableCell>
<TableCell>
{channel.created ? new Date(channel.created).toLocaleDateString() : "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(channel as AlertConfiguration)}
disabled={channel.isWebhook}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
if (channel.id && confirm("Are you sure you want to delete this notification channel?")) {
onDelete(channel.id)
}
}}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};