feat: Add notification, integrate data retention service, and improve i18n support (#164)
* fix(ssl): Ensure edit form saves notification_id and template_id in DB. - The SSL edit form was not saving the `notification_id` and `template_id` fields to the PB database when re-assigning the Notification Channel and Alert Template. * chore(partners): add Cloudflare to Ecosystem & Community Partner section - Cloudflare has been added to the Ecosystem & Community Partner list to acknowledge their support and contribution. * ⏳ chore(project): update development status and progress - fix(ssl): ensure edit form saves notification_id and template_id in DB - feat(notifications): add Notifiarr notification channel (planned) * feat: Add Notifiarr to notification channels (UI) - Add Notifiarr as a new channel type in the notification settings. The Notifiarr channel will use the `api_token` field for API key storage in PB. * feat: Add channel_id field for Notifiarr - Add channel_id field for Notifiarr to store record of the discord channel_id into the PB. * feat: Implement Notifiarr notification service - The Notifiarr notification functionality has been implemented. The alert configuration channel will now use the `api_token` and `channel_id` field for send to the Notifiarr API endpoint. * ⏳ chore(project): update development status and progress - feat: Add channel_id field for Notifiarr - feat: Implement Notifiarr notification service Closes: #133 * feat: add SSL history and Notifiarr notification channel schema * Fix: Remove duplicate close button - Removed the duplicate close button from the test email dialog. * feat: Add Gotify to notification channels - Add Gotify as a new channel type in the notification settings. The Gotify channel will use the `api_token` and `server_url` field for API key storage in PB. * feat: Implement Gotify notification service - The Gotify notification functionality has been implemented. The alert configuration channel will now use the `api_token` and `server_url` field for send to the Gotify API endpoint. * ⏳ chore(project): update development status and progress - feat: Add server_url field for Gotify - feat: Implement Gotify notification service * refactory(i18n): Improve internationalization configuration and add new translations - Added multiple new translation items - Updated the two language files: en and zhcn - Added type definitions for new translation items in types/services.ts - Modified some components to use the new translation items * refactory(i18n): Added architecture import function translation for the "About" page - Added translation items related to schema import in English, Chinese, and type definition files - Replaced hard-coded text with new translation items in the AboutSystem component * refactory(i18n): Added architecture import function translation for the "About" page - Added translation items related to schema import in English, Chinese, and type definition files - Replaced hard-coded text with new translation items in the AboutSystem component * refactory(i18n): Add internationalization support for the service statistics card * feat: Add NTFY API token field for Token-based authentication to ntfy server - Add the api_token field for NTFY notifications in both the dialog and service to save it to PB. NTFY schema to include api_token field. * feat(auth): add token support for ntfy - Updated the NTFY service to support API token authentication by adding Authorization Bearer header when api_token is provided. Closes: #89 * refactor(i18n): Added Chinese translations for Create Event and Maintenance - Supplemented missing Chinese translations - Added translation entries related to Create Event and Maintenance in both English and Chinese translation files * chore: deactivate GitHub donation * refactor(i18n): Optimize internationalization configuration and add new translation items - Add internationalization support to the LoadingState component - Implement internationalization translations in the Profile page - Use internationalized text in the TestEmailDialog component - Update English and Chinese translation files by adding new translation entries * docs(readme): deactivate donations, accept only infra support (cloud, domain, hosting) * feat(i18n): Enhanced internationalization support with variable interpolation - Refactored the `t` function in LanguageContext to support multiple parameter combinations - Added variable interpolation feature, allowing variables in translated texts - Optimized translation logic to improve accuracy and performance - Enhanced type safety by using TypeScript generics to ensure correct typing * refactor(i18n): Optimize service pagination internationalization copy - Added service pagination related copy in English and Chinese translation files - Updated type definitions to include new translation fields - Modified the ServicesPagination component to use the newly added translation fields * docs(README_zhcn): Update documentation content and sponsorship policy - Update image links in the documentation - Revise the sponsorship policy to no longer accept individual sponsorships, only enterprise-level collaborations - Update the display method for supporters and partners - Add Cloudflare and DigitalOcean as new partners * feat: Integrate data retention service - Implemented the data retention service in Go (service-operation) that manages cleanup of old records based on configured retention periods. The service runs once per day to ensure outdated data is removed automatically. * refactor: Remove manual cleanup buttons from Data Retention Settings - The three manual cleanup buttons have been removed from the dashboard. - The data retention service now automatically runs once per day, cleaning up older data based on the configured retention settings. --------- Co-authored-by: YiZixuan <sqkkyzx@qq.com> Co-authored-by: YiZixuan <sqkkyzx@outlook.com>
This commit is contained in:
@@ -153,7 +153,8 @@
|
||||
"google_chat",
|
||||
"email",
|
||||
"ntfy",
|
||||
"pushover"
|
||||
"pushover",
|
||||
"notifiarr"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4823,6 +4824,169 @@
|
||||
"indexes": [],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "pbc_1753185970",
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
"name": "ssl_history",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text2812878347",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "cert_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1881225410",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "days_before_expiry",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3062427817",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "days_before_warning",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text98176952",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "template_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text4011498884",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "notification_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "select3839288252",
|
||||
"maxSelect": 1,
|
||||
"name": "warning_status",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"sent",
|
||||
"pending",
|
||||
"failed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "select2670434693",
|
||||
"maxSelect": 1,
|
||||
"name": "expiry_status",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"sent",
|
||||
"pending",
|
||||
"failed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "select3824245652",
|
||||
"maxSelect": 1,
|
||||
"name": "critical_status",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"sent",
|
||||
"pending",
|
||||
"failed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "pbc_351174173",
|
||||
"listRule": "",
|
||||
@@ -5479,4 +5643,4 @@
|
||||
"indexes": [],
|
||||
"system": false
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
@@ -2,6 +2,7 @@
|
||||
import React from "react";
|
||||
import { Clock, TimerOff } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface LastCheckedTimeProps {
|
||||
lastCheckedTime: string;
|
||||
@@ -10,6 +11,7 @@ interface LastCheckedTimeProps {
|
||||
}
|
||||
|
||||
export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheckedTimeProps) => {
|
||||
const { t } = useLanguage();
|
||||
// Format the time without seconds to display a static time
|
||||
const formatTimeWithoutSeconds = (timeString: string) => {
|
||||
try {
|
||||
@@ -55,7 +57,7 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
|
||||
<Clock className="h-4 w-4" />
|
||||
)}
|
||||
<span>
|
||||
{isPaused ? "Paused at " : ""}
|
||||
{isPaused ? t("pausedAt") : ""}
|
||||
{formattedTime}
|
||||
</span>
|
||||
</div>
|
||||
@@ -66,14 +68,14 @@ export const LastCheckedTime = ({ lastCheckedTime, status, interval }: LastCheck
|
||||
>
|
||||
<div className="flex flex-col gap-1 text-xs">
|
||||
<div className="font-medium">
|
||||
{isPaused ? "Monitoring Paused" : "Last Check Details"}
|
||||
{isPaused ? t("monitoringPaused") : t("lastCheckDetails")}
|
||||
</div>
|
||||
<div>
|
||||
{isPaused ? "No automatic checks" : `Checked at ${formattedTime}`}
|
||||
{isPaused ? t("noAutomaticChecks") : t("checkedAt") + `${formattedTime}`}
|
||||
</div>
|
||||
{interval && !isPaused && (
|
||||
<div>
|
||||
Check interval: {formatInterval(interval)}
|
||||
{t("checkInterval")}: {formatInterval(interval)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-gray-400 text-[10px]">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
export function LoadingState() {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-background text-foreground">
|
||||
<div className="flex flex-col items-center text-center py-8 px-4 rounded-lg shadow-lg bg-card animate-fade-in">
|
||||
@@ -7,8 +9,8 @@ export function LoadingState() {
|
||||
<div className="absolute w-12 h-12 rounded-full border-4 border-primary/20"></div>
|
||||
<div className="w-12 h-12 rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div>
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-1">Loading server data</h3>
|
||||
<p className="text-muted-foreground">Please wait while we retrieve your information...</p>
|
||||
<h3 className="text-xl font-medium mb-1">{t("loadingServerData")}</h3>
|
||||
<p className="text-muted-foreground">{t("retrievingYourInformation")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Service } from "@/types/service.types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServiceEditDialogProps {
|
||||
open: boolean;
|
||||
@@ -19,6 +20,7 @@ interface ServiceEditDialogProps {
|
||||
}
|
||||
|
||||
export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDialogProps) {
|
||||
const { t } = useLanguage();
|
||||
const queryClient = useQueryClient();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@@ -53,9 +55,9 @@ export function ServiceEditDialog({ open, onOpenChange, service }: ServiceEditDi
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Edit Service</DialogTitle>
|
||||
<DialogTitle className="text-xl">{t("editService")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the details of your monitored service.
|
||||
{t("editServiceDesc")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{open && service && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Service, UptimeData } from "@/types/service.types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useMemo } from "react";
|
||||
import { formatDistanceStrict } from "date-fns";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceStatsCardsProps {
|
||||
service: Service;
|
||||
@@ -11,6 +12,8 @@ interface ServiceStatsCardsProps {
|
||||
}
|
||||
|
||||
export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProps) {
|
||||
|
||||
const { t } = useLanguage();
|
||||
// Calculate uptime percentage
|
||||
const calculateUptimePercentage = () => {
|
||||
if (uptimeData.length === 0) return 100;
|
||||
@@ -109,41 +112,46 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Card className="border-blue-400 dark:border-blue-700 bg-blue-50 dark:bg-blue-900/50 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">Response Time</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-blue-800 dark:text-blue-100">{t('responseTime')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-700 dark:text-white">
|
||||
{service.responseTime}ms
|
||||
</div>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Last checked at {service.lastChecked}</p>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('lastCheckedAt').replace('{datetime}', service.lastChecked)}</p>
|
||||
{avgResponseTime > 0 && avgResponseTime !== service.responseTime && (
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">Avg: {avgResponseTime}ms (last {upChecks.length} up checks)</p>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-200 mt-1">{t('avg')}: {avgResponseTime}ms ({t('lastUpChecksCount').replace('{count}', String(upChecks.length))})</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-green-400 dark:border-green-700 bg-green-50 dark:bg-green-900/50 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">Uptime</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-green-800 dark:text-green-100">{t('uptime')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-700 dark:text-white">
|
||||
{uptimePercentage}%
|
||||
</div>
|
||||
<p className="text-xs text-green-600 dark:text-green-200 mt-1">Based on last {uptimeData.length} checks</p>
|
||||
<p className="text-xs text-green-600 dark:text-green-200 mt-1">{t('basedOnlastChecksCount').replace('{count}', String(uptimeData.length))}</p>
|
||||
<div className="flex flex-col space-y-1 mt-2">
|
||||
<div className="flex items-center text-xs text-green-600 dark:text-green-200">
|
||||
<ArrowUp className="h-3 w-3 mr-1 text-green-600 dark:text-green-400" />
|
||||
<span>Total uptime: {uptimeStats.totalUptimeFormatted}</span>
|
||||
<span>{t("totalUptime")}: {uptimeStats.totalUptimeFormatted}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-red-600 dark:text-red-200">
|
||||
<ArrowDown className="h-3 w-3 mr-1 text-red-600 dark:text-red-400" />
|
||||
<span>Total downtime: {uptimeStats.totalDowntimeFormatted}</span>
|
||||
<span>{t("totalDowntime")}: {uptimeStats.totalDowntimeFormatted}</span>
|
||||
</div>
|
||||
{service.status !== "paused" && (
|
||||
<div className="flex items-center text-xs font-medium mt-1">
|
||||
<span className={service.status === "up" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
|
||||
{service.status === "up" ? "Up" : "Down"} for {uptimeStats.currentStatusDuration}
|
||||
{
|
||||
service.status === "up" ?
|
||||
t('upStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
|
||||
:
|
||||
t('downStatusDuration').replace("{duration}", uptimeStats.currentStatusDuration)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -153,16 +161,16 @@ export function ServiceStatsCards({ service, uptimeData }: ServiceStatsCardsProp
|
||||
|
||||
<Card className="border-purple-400 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/50 shadow-md">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">Monitoring Settings</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-purple-800 dark:text-purple-100">{t('monitoringSettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200">
|
||||
<Clock className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
||||
<span>Checked every {service.interval} seconds</span>
|
||||
<span>{t('monitoringSettingsInterval').replace('{interval}', String(service.interval))}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-purple-700 dark:text-purple-200 mt-1">
|
||||
<Server className="h-4 w-4 mr-2 text-purple-600 dark:text-purple-400" />
|
||||
<span>{service.type} monitoring</span>
|
||||
<span>{service.type} {t('monitoringSettingsType')}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -80,9 +80,9 @@ export function ServicesPagination({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{totalItems > 0
|
||||
? `${startItem}-${endItem} of ${totalItems} ${t("services") || "services"}`
|
||||
: `0 ${t("services") || "services"}`
|
||||
{totalItems > 0
|
||||
? t("servicesPagination", {"startItem": startItem, "endItem": endItem, "totalItems": totalItems})
|
||||
: t("servicesPaginationNoService")
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { UptimeStatusItem } from './uptime/UptimeStatusItem';
|
||||
import { uptimeService } from '@/services/uptimeService';
|
||||
import { UptimeData } from '@/types/service.types';
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface UptimeBarProps {
|
||||
uptime: number;
|
||||
@@ -15,6 +16,7 @@ interface UptimeBarProps {
|
||||
}
|
||||
|
||||
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
|
||||
const { t } = useLanguage();
|
||||
// Calculate date range for last 20 checks with much more aggressive caching
|
||||
const endDate = useMemo(() => new Date(), []);
|
||||
const startDate = useMemo(() => {
|
||||
@@ -84,7 +86,7 @@ const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType =
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Last 20 checks
|
||||
{t('last20Checks')}
|
||||
</span>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { MouseEvent } from "react";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface ServiceFormActionsProps {
|
||||
isSubmitting: boolean;
|
||||
@@ -14,6 +15,8 @@ export function ServiceFormActions({
|
||||
onCancel,
|
||||
submitLabel = "Create Service"
|
||||
}: ServiceFormActionsProps) {
|
||||
|
||||
const {t} = useLanguage();
|
||||
const handleCancel = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!isSubmitting) {
|
||||
@@ -29,7 +32,7 @@ export function ServiceFormActions({
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -38,7 +41,7 @@ export function ServiceFormActions({
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Processing...
|
||||
{t("processing")}...
|
||||
</>
|
||||
) : (
|
||||
submitLabel
|
||||
|
||||
@@ -25,8 +25,8 @@ export function IncidentTable({ incidents }: IncidentTableProps) {
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("time")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("status")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("responseTime")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Error Message</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>Details</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("errorMessage")}</TableHead>
|
||||
<TableHead className={theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}>{t("details")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -9,10 +9,12 @@ import { IncidentTable } from "./IncidentTable";
|
||||
import { StatusFilter, PageSize } from "./types";
|
||||
import { getStatusChangeEvents } from "./utils";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] }) {
|
||||
// Get current theme
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Filter state
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
@@ -65,7 +67,7 @@ export function LatestChecksTable({ uptimeData }: { uptimeData: UptimeData[] })
|
||||
<CardHeader>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<CardTitle className="text-card-foreground">
|
||||
<span>Incident History</span>
|
||||
<span>{t("incidentHistory")}</span>
|
||||
</CardTitle>
|
||||
<StatusFilterTabs statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} />
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { PageSize } from "./types";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface TablePaginationProps {
|
||||
currentPage: number;
|
||||
@@ -35,6 +36,7 @@ export function TablePagination({
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}: TablePaginationProps) {
|
||||
const {t} = useLanguage()
|
||||
// Generate page numbers
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
@@ -58,7 +60,7 @@ export function TablePagination({
|
||||
return (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">Rows per page:</span>
|
||||
<span className="text-sm text-muted-foreground">{t("rowsPerPage")}:</span>
|
||||
<Select
|
||||
value={pageSize}
|
||||
onValueChange={(value) => onPageSizeChange(value as PageSize)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { Service } from "@/types/service.types";
|
||||
import { serviceService } from "@/services/serviceService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface ServiceRowActionsProps {
|
||||
service: Service;
|
||||
@@ -31,6 +32,7 @@ export const ServiceRowActions = ({
|
||||
onMuteAlerts
|
||||
}: ServiceRowActionsProps) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Handle pause/resume directly from dropdown
|
||||
const handlePauseResume = async (e: React.MouseEvent) => {
|
||||
@@ -122,7 +124,7 @@ export const ServiceRowActions = ({
|
||||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
<span>View Detail</span>
|
||||
<span>{t("viewDetail")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
||||
@@ -131,12 +133,12 @@ export const ServiceRowActions = ({
|
||||
{service.status === "paused" ? (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
<span>Resume Monitoring</span>
|
||||
<span>{t("resumeMonitoring")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="h-4 w-4" />
|
||||
<span>Pause Monitoring</span>
|
||||
<span>{t("pauseMonitoring")}</span>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -148,7 +150,7 @@ export const ServiceRowActions = ({
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
<span>{t("edit")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer text-base py-2.5"
|
||||
@@ -157,12 +159,12 @@ export const ServiceRowActions = ({
|
||||
{alertsMuted ? (
|
||||
<>
|
||||
<Bell className="h-4 w-4" />
|
||||
<span>Unmute Alerts</span>
|
||||
<span>{t("unmuteAlerts")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BellOff className="h-4 w-4" />
|
||||
<span>Mute Alerts</span>
|
||||
<span>{t("muteAlerts")}</span>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -175,7 +177,7 @@ export const ServiceRowActions = ({
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
<span>{t("delete")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
interface UptimeSummaryProps {
|
||||
uptime: number;
|
||||
@@ -7,13 +8,14 @@ interface UptimeSummaryProps {
|
||||
}
|
||||
|
||||
export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className="flex items-center justify-between text-xs mt-1">
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(uptime)}% uptime
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Last 20 checks
|
||||
{t('last20Checks')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -355,9 +355,9 @@ export const AboutSystem: React.FC = () => {
|
||||
<CardHeader className="bg-muted/50 pb-4">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className={`h-5 w-5 ${theme === 'dark' ? 'text-blue-400' : 'text-blue-600'}`} />
|
||||
<span>Update Schema</span>
|
||||
<span>{t('updateSchema')}</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="font-medium text-base">Automatic import collections schema</CardDescription>
|
||||
<CardDescription className="font-medium text-base">{t('updateSchemaDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
<div className="space-y-3">
|
||||
@@ -368,7 +368,7 @@ export const AboutSystem: React.FC = () => {
|
||||
onCheckedChange={(checked) => setMergeFields(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="merge-fields" className="text-sm font-medium">
|
||||
Merge fields with existing collections (safe - preserves data)
|
||||
{t('mergeFieldsLabel')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -382,7 +382,7 @@ export const AboutSystem: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
<Database className={`h-4 w-4 ${isImporting ? 'animate-spin' : ''}`} />
|
||||
{isImporting ? 'Importing...' : 'Click to update Schema'}
|
||||
{isImporting ? t('importing') : t('clickToUpdateSchema')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -403,7 +403,7 @@ export const AboutSystem: React.FC = () => {
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: 'text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{importResult.success ? 'Import Successful' : 'Import Failed'}
|
||||
{importResult.success ? t('importSuccessful') : t('importFailed')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`mt-2 text-sm ${
|
||||
@@ -411,21 +411,21 @@ export const AboutSystem: React.FC = () => {
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: 'text-red-700 dark:text-red-300'
|
||||
}`}>
|
||||
{importResult.created > 0 && `${importResult.created} collections created`}
|
||||
{importResult.updated > 0 && (importResult.created > 0 ? ', ' : '') + `${importResult.updated} collections updated`}
|
||||
{importResult.skipped > 0 && ((importResult.created > 0 || importResult.updated > 0) ? ', ' : '') + `${importResult.skipped} collections skipped`}
|
||||
{importResult.created > 0 && t('collectionsCreatedCount').replace('{count}', String(importResult.created))}
|
||||
{importResult.updated > 0 && (importResult.created > 0 ? ', ' : '') + t('collectionsUpdatedCount').replace('{count}', String(importResult.updated))}
|
||||
{importResult.skipped > 0 && ((importResult.created > 0 || importResult.updated > 0) ? ', ' : '') + t('collectionsSkippedCount').replace('{count}', String(importResult.skipped))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="mb-2">
|
||||
<strong>Instructions:</strong>
|
||||
<strong>{t('instructions')}:</strong>
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>Merge fields:</strong> Safely add new fields to existing collections, preserves all data</li>
|
||||
<li>System collections (starting with _) and users collection will be skipped automatically</li>
|
||||
<li>Only authenticated admins can perform schema imports</li>
|
||||
<li><strong>{t('mergeFields')}:</strong> {t('instructionsMergeFields')}</li>
|
||||
<li>{t('instructionsCollections')}</li>
|
||||
<li>{t('instructionsImportAuth')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { TemplateType, templateTypeConfigs } from "@/services/templateService";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {z} from "zod";
|
||||
|
||||
interface TemplateDialogProps {
|
||||
open: boolean;
|
||||
@@ -81,6 +82,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
||||
};
|
||||
|
||||
const renderPlaceholderGuide = () => {
|
||||
|
||||
const config = templateTypeConfigs[selectedTemplateType];
|
||||
if (!config) return null;
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ const DataRetentionSettings = () => {
|
||||
setLastCleanup(result.lastCleanup);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading retention settings:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load retention settings",
|
||||
@@ -72,7 +71,6 @@ const DataRetentionSettings = () => {
|
||||
description: "Data retention settings have been updated",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving retention settings:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to save retention settings",
|
||||
@@ -96,7 +94,6 @@ const DataRetentionSettings = () => {
|
||||
// Reload settings to get updated last cleanup time
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("Error during uptime cleanup:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to perform uptime data cleanup",
|
||||
@@ -120,7 +117,6 @@ const DataRetentionSettings = () => {
|
||||
// Reload settings to get updated last cleanup time
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("Error during server cleanup:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to perform server data cleanup",
|
||||
@@ -144,7 +140,6 @@ const DataRetentionSettings = () => {
|
||||
// Reload settings to get updated last cleanup time
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
console.error("Error during manual cleanup:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to perform database cleanup",
|
||||
@@ -170,7 +165,7 @@ const DataRetentionSettings = () => {
|
||||
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
||||
<AlertTriangle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<AlertDescription className="text-blue-700 dark:text-blue-300">
|
||||
<span className="font-medium">{t("permissionNotice")}</span> As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.
|
||||
<span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
@@ -210,10 +205,16 @@ const DataRetentionSettings = () => {
|
||||
min="1"
|
||||
max="365"
|
||||
value={settings.uptimeRetentionDays}
|
||||
onChange={(e) => setSettings(prev => ({
|
||||
...prev,
|
||||
uptimeRetentionDays: parseInt(e.target.value) || 30
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
uptimeRetentionDays: Number(value)
|
||||
}));
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -229,10 +230,16 @@ const DataRetentionSettings = () => {
|
||||
min="1"
|
||||
max="365"
|
||||
value={settings.serverRetentionDays}
|
||||
onChange={(e) => setSettings(prev => ({
|
||||
...prev,
|
||||
serverRetentionDays: parseInt(e.target.value) || 30
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || isNaN(Number(value))) {
|
||||
return;
|
||||
}
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
serverRetentionDays: Number(value)
|
||||
}));
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -250,63 +257,17 @@ const DataRetentionSettings = () => {
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2 w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleUptimeShrink}
|
||||
disabled={isUptimeShrinking}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isUptimeShrinking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Globe className="h-4 w-4" />
|
||||
)}
|
||||
Shrink Uptime Data
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleServerShrink}
|
||||
disabled={isServerShrinking}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isServerShrinking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Server className="h-4 w-4" />
|
||||
)}
|
||||
Shrink Server Data
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleFullShrink}
|
||||
disabled={isFullShrinking}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isFullShrinking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4" />
|
||||
)}
|
||||
Full Database Shrink
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -126,15 +126,6 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
||||
<Mail className="h-5 w-5" />
|
||||
{t("sendTestEmail", "settings")}
|
||||
</DialogTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-4 top-4"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
@@ -203,9 +194,7 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
||||
{/* Info message */}
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.
|
||||
</AlertDescription>
|
||||
<AlertDescription>{t("testEmailAlert", "settings")}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
@@ -223,7 +212,7 @@ const TestEmailDialog: React.FC<TestEmailDialogProps> = ({
|
||||
) : (
|
||||
<Mail className="h-4 w-4" />
|
||||
)}
|
||||
{isLoading ? t("sending", "settings") : t("send", "common")}
|
||||
{isLoading ? t("sending", "settings") : t("send", "settings")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
+142
-20
@@ -27,6 +27,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Loader2, Copy } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
|
||||
interface NotificationChannelDialogProps {
|
||||
open: boolean;
|
||||
@@ -34,9 +35,10 @@ interface NotificationChannelDialogProps {
|
||||
editingConfig: AlertConfiguration | null;
|
||||
}
|
||||
|
||||
|
||||
const baseSchema = z.object({
|
||||
notify_name: z.string().min(1, "Name is required"),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "webhook"]),
|
||||
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "notifiarr", "gotify", "webhook"]),
|
||||
enabled: z.boolean().default(true),
|
||||
service_id: z.string().default("global"),
|
||||
template_id: z.string().optional(),
|
||||
@@ -81,6 +83,19 @@ const emailSchema = baseSchema.extend({
|
||||
const ntfySchema = baseSchema.extend({
|
||||
notification_type: z.literal("ntfy"),
|
||||
ntfy_endpoint: z.string().url("Must be a valid NTFY endpoint URL"),
|
||||
api_token: z.string().optional(),
|
||||
});
|
||||
|
||||
const pushoverSchema = baseSchema.extend({
|
||||
notification_type: z.literal("pushover"),
|
||||
api_token: z.string().min(1, "API token is required"),
|
||||
user_key: z.string().min(1, "User key is required"),
|
||||
});
|
||||
|
||||
const notifiarrSchema = baseSchema.extend({
|
||||
notification_type: z.literal("notifiarr"),
|
||||
api_token: z.string().min(1, "API token is required"),
|
||||
channel_id: z.string().min(1, "Channel ID is required"),
|
||||
});
|
||||
|
||||
const webhookSchema = baseSchema.extend({
|
||||
@@ -89,10 +104,10 @@ const webhookSchema = baseSchema.extend({
|
||||
webhook_payload_template: z.string().optional(),
|
||||
});
|
||||
|
||||
const pushoverSchema = baseSchema.extend({
|
||||
notification_type: z.literal("pushover"),
|
||||
const gotifySchema = baseSchema.extend({
|
||||
notification_type: z.literal("gotify"),
|
||||
api_token: z.string().min(1, "API token is required"),
|
||||
user_key: z.string().min(1, "User key is required"),
|
||||
server_url: z.string().url("Must be a valid server URL"),
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("notification_type", [
|
||||
@@ -104,6 +119,8 @@ const formSchema = z.discriminatedUnion("notification_type", [
|
||||
emailSchema,
|
||||
ntfySchema,
|
||||
pushoverSchema,
|
||||
notifiarrSchema,
|
||||
gotifySchema,
|
||||
webhookSchema,
|
||||
]);
|
||||
|
||||
@@ -158,6 +175,19 @@ const notificationTypeOptions = [
|
||||
description: "Send push notifications via Pushover",
|
||||
icon: "/upload/notification/pushover.png"
|
||||
},
|
||||
|
||||
{
|
||||
value: "notifiarr",
|
||||
label: "Notifiarr",
|
||||
description: "Send notifications via Notifiarr",
|
||||
icon: "/upload/notification/notifiarr.png"
|
||||
},
|
||||
{
|
||||
value: "gotify",
|
||||
label: "Gotify",
|
||||
description: "Send push notifications via Gotify",
|
||||
icon: "/upload/notification/gotify.png"
|
||||
},
|
||||
{
|
||||
value: "webhook",
|
||||
label: "Webhook",
|
||||
@@ -566,22 +596,40 @@ export const NotificationChannelDialog = ({
|
||||
)}
|
||||
|
||||
{notificationType === "ntfy" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ntfy_endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>NTFY Endpoint</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ntfy_endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>NTFY Endpoint</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter NTFY API token" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional API token for authentication with NTFY server
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "pushover" && (
|
||||
@@ -620,6 +668,80 @@ export const NotificationChannelDialog = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "notifiarr" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Notifiarr API token" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Notifiarr API token for sending notifications
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channel_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Discord Channel ID" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The Discord channel ID where notifications will be sent
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "gotify" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Gotify API token" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Gotify application API token
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="server_url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://your-gotify-server.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The URL of your Gotify server
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notificationType === "webhook" && (
|
||||
<>
|
||||
|
||||
@@ -67,6 +67,8 @@ export const NotificationChannelList = ({
|
||||
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";
|
||||
default: return type || "Unknown";
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ export const SSLCertificatesTable = () => {
|
||||
warning_threshold: updatedCertificate.warning_threshold,
|
||||
expiry_threshold: updatedCertificate.expiry_threshold,
|
||||
notification_channel: updatedCertificate.notification_channel,
|
||||
notification_id: updatedCertificate.notification_id,
|
||||
template_id: updatedCertificate.template_id,
|
||||
check_interval: updatedCertificate.check_interval,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react";
|
||||
import { translations, Language, TranslationModule, TranslationKey } from "@/translations";
|
||||
|
||||
type TranslationVars = Record<string, string | number>;
|
||||
|
||||
type LanguageContextType = {
|
||||
language: Language;
|
||||
setLanguage: (language: Language) => void;
|
||||
t: <M extends TranslationModule>(key: string, module?: M) => string;
|
||||
t: {
|
||||
(key: string): string;
|
||||
(key: string, vars: TranslationVars): string;
|
||||
<M extends TranslationModule>(key: string, module: M): string;
|
||||
<M extends TranslationModule>(key: string, module: M, vars: TranslationVars): string;
|
||||
};
|
||||
};
|
||||
|
||||
// ❗ Create the context with `undefined` to enforce provider usage
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
// ✅ Stable custom hook
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useLanguage = () => {
|
||||
const context = useContext(LanguageContext);
|
||||
if (!context) {
|
||||
@@ -23,33 +29,76 @@ export const LanguageProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [language, setLanguage] = useState<Language>("en");
|
||||
const fallbackLanguage: Language = "en";
|
||||
|
||||
const t = useCallback(<M extends TranslationModule>(key: string, module?: M): string => {
|
||||
const interpolate = (template: string, vars: TranslationVars): string => {
|
||||
return template.replace(/{(\w+)}/g, (match, key) => {
|
||||
return vars[key] !== undefined ? String(vars[key]) : match;
|
||||
});
|
||||
};
|
||||
|
||||
const t = useCallback(((
|
||||
key: string,
|
||||
moduleOrVars?: TranslationModule | TranslationVars,
|
||||
vars?: TranslationVars
|
||||
): string => {
|
||||
let module: TranslationModule | undefined;
|
||||
let variables: TranslationVars | undefined;
|
||||
|
||||
if (typeof moduleOrVars === "string") {
|
||||
module = moduleOrVars;
|
||||
variables = vars;
|
||||
} else if (typeof moduleOrVars === "object") {
|
||||
variables = moduleOrVars;
|
||||
}
|
||||
|
||||
const langPack = translations[language];
|
||||
const fallbackPack = translations[fallbackLanguage];
|
||||
let translatedText = "";
|
||||
|
||||
if (module) {
|
||||
const valCur = langPack?.[module]?.[key as TranslationKey<M>];
|
||||
if (typeof valCur === "string") return valCur;
|
||||
const moduleKey = key as TranslationKey<typeof module>;
|
||||
const valCur = langPack?.[module]?.[moduleKey];
|
||||
if (typeof valCur === "string") {
|
||||
translatedText = valCur;
|
||||
} else {
|
||||
const valEn = fallbackPack?.[module]?.[moduleKey];
|
||||
translatedText = typeof valEn === "string" ? valEn : key;
|
||||
}
|
||||
} else {
|
||||
let found = false;
|
||||
|
||||
const valEn = fallbackPack?.[module]?.[key as TranslationKey<M>];
|
||||
if (typeof valEn === "string") return valEn;
|
||||
for (const mod of Object.keys(langPack) as TranslationModule[]) {
|
||||
const moduleTranslations = langPack[mod];
|
||||
if (moduleTranslations && key in moduleTranslations) {
|
||||
const val = moduleTranslations[key as keyof typeof moduleTranslations];
|
||||
if (typeof val === "string") {
|
||||
translatedText = val;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
if (!found) {
|
||||
for (const mod of Object.keys(fallbackPack) as TranslationModule[]) {
|
||||
const moduleTranslations = fallbackPack[mod];
|
||||
if (moduleTranslations && key in moduleTranslations) {
|
||||
const val = moduleTranslations[key as keyof typeof moduleTranslations];
|
||||
if (typeof val === "string") {
|
||||
translatedText = val;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
translatedText = key;
|
||||
}
|
||||
}
|
||||
|
||||
for (const mod in langPack) {
|
||||
const m = mod as TranslationModule;
|
||||
const val = langPack[m]?.[key as any];
|
||||
if (typeof val === "string") return val;
|
||||
}
|
||||
|
||||
for (const mod in fallbackPack) {
|
||||
const m = mod as TranslationModule;
|
||||
const val = fallbackPack[m]?.[key as any];
|
||||
if (typeof val === "string") return val;
|
||||
}
|
||||
|
||||
return key;
|
||||
}, [language]);
|
||||
return variables ? interpolate(translatedText, variables) : translatedText;
|
||||
}) as LanguageContextType['t'], [language]);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ language, setLanguage, t }}>
|
||||
|
||||
@@ -10,8 +10,11 @@ import { User } from "@/services/userService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import {useLanguage} from "@/contexts/LanguageContext.tsx";
|
||||
|
||||
const Profile = () => {
|
||||
const { t } = useLanguage()
|
||||
|
||||
// Use shared sidebar state
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
|
||||
@@ -84,12 +87,12 @@ const Profile = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<p>Please login to view your profile</p>
|
||||
<p>{t("loginToViewProfile")}</p>
|
||||
<button
|
||||
onClick={() => navigate("/login")}
|
||||
className="mt-4 px-4 py-2 bg-primary text-white rounded"
|
||||
>
|
||||
Go to Login
|
||||
{t("goToLogin")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +113,7 @@ const Profile = () => {
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<span className="ml-2">Loading user data...</span>
|
||||
<span className="ml-2">{t("loadingUserData")}</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
@@ -121,7 +124,7 @@ const Profile = () => {
|
||||
onClick={() => fetchUserData()}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
|
||||
>
|
||||
Retry
|
||||
{t("retry")}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -136,4 +139,4 @@ const Profile = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
export default Profile;
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
|
||||
collectionId?: string;
|
||||
collectionName?: string;
|
||||
service_id: string;
|
||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "webhook";
|
||||
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "notifiarr" | "gotify" | "webhook";
|
||||
telegram_chat_id?: string;
|
||||
discord_webhook_url?: string;
|
||||
signal_number?: string;
|
||||
@@ -31,6 +31,7 @@ export interface AlertConfiguration {
|
||||
ntfy_endpoint?: string;
|
||||
api_token?: string;
|
||||
user_key?: string;
|
||||
server_url?: string;
|
||||
webhook_url?: string;
|
||||
webhook_payload_template?: string;
|
||||
}
|
||||
@@ -87,10 +88,19 @@ export const alertConfigService = {
|
||||
|
||||
} else if (config.notification_type === "ntfy") {
|
||||
cleanConfig.ntfy_endpoint = config.ntfy_endpoint || "";
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
|
||||
} else if (config.notification_type === "pushover") {
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
cleanConfig.user_key = config.user_key || "";
|
||||
|
||||
} else if (config.notification_type === "notifiarr") {
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
cleanConfig.channel_id = config.channel_id || "";
|
||||
|
||||
} else if (config.notification_type === "gotify") {
|
||||
cleanConfig.api_token = config.api_token || "";
|
||||
cleanConfig.server_url = config.server_url || "";
|
||||
|
||||
} else if (config.notification_type === "webhook") {
|
||||
cleanConfig.webhook_url = config.webhook_url || "";
|
||||
|
||||
@@ -16,4 +16,19 @@ export const aboutTranslations: AboutTranslations = {
|
||||
quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.",
|
||||
quickTips: "Quick Tips",
|
||||
releasedOn: "Released On",
|
||||
updateSchema: "Update Schema",
|
||||
updateSchemaDesc: "Automatic import collections schema",
|
||||
mergeFieldsLabel: "Merge fields with existing collections (safe - preserves data)",
|
||||
importing: "Importing...",
|
||||
clickToUpdateSchema: "Click to update Schema",
|
||||
importSuccessful: "Import Successful",
|
||||
importFailed: "Import Failed",
|
||||
instructions: "Instructions",
|
||||
mergeFields: "Merge fields",
|
||||
instructionsMergeFields: "Safely add new fields to existing collections, preserves all data",
|
||||
instructionsCollections: "System collections (starting with _) and users collection will be skipped automatically",
|
||||
instructionsImportAuth: "Only authenticated admins can perform schema imports",
|
||||
collectionsUpdatedCount: "{count} collections updated",
|
||||
collectionsCreatedCount: "{count} collections created",
|
||||
collectionsSkippedCount: "{count} collections created",
|
||||
};
|
||||
|
||||
@@ -61,4 +61,5 @@ export const incidentTranslations: IncidentTranslations = {
|
||||
affectedSystems: 'Affected Systems',
|
||||
enterAffectedSystems: 'Enter Affected Systems',
|
||||
separateSystemsWithComma: 'Enter affected systems seperated by comma',
|
||||
createIncidentDesc: 'Create a new incident',
|
||||
};
|
||||
|
||||
@@ -78,4 +78,12 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
selectNotificationChannel: 'Add a notification channel',
|
||||
enableNotificationsFirst: 'Enable notifications first to select channel',
|
||||
updateMaintenance: 'Update Maintenance',
|
||||
loginToViewProfile: 'Please login to view your profile',
|
||||
goToLogin: 'Go to Login',
|
||||
loadingUserData: 'Loading user data...',
|
||||
retry: 'Retry',
|
||||
loadingServerData: 'Loading server data...',
|
||||
retrievingYourInformation: 'Please wait while we retrieve your information...',
|
||||
servicesPagination: '{startItem}-{endItem} of {totalItems} services',
|
||||
servicesPaginationNoService: '0 services',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ServicesTranslations } from '../types/services';
|
||||
|
||||
export const servicesTranslations: ServicesTranslations = {
|
||||
serviceStatus: "Service Status",
|
||||
responseTime: "Response Time",
|
||||
uptime: "Uptime",
|
||||
lastChecked: "Last Checked",
|
||||
noServices: "No services match your filter criteria.",
|
||||
@@ -95,4 +94,46 @@ export const servicesTranslations: ServicesTranslations = {
|
||||
downServices: "DOWN SERVICES",
|
||||
pausedServices: "PAUSED SERVICES",
|
||||
warningServices: "WARNING SERVICES",
|
||||
|
||||
// ServiceRowActions.tsx
|
||||
viewDetail: "View Detail",
|
||||
resumeMonitoring: "Resume Monitoring",
|
||||
pauseMonitoring: "Pause Monitoring",
|
||||
|
||||
|
||||
//IncidentTable.tsx
|
||||
responseTime: "Response Time",
|
||||
errorMessage: "Error Message",
|
||||
details: "Details",
|
||||
unmuteAlerts: "Unmute Alerts",
|
||||
muteAlerts: "Mute Alerts",
|
||||
|
||||
//LastCheckedTime.tsx
|
||||
pausedAt: "Paused at ",
|
||||
lastCheckDetails: "Last Check Details",
|
||||
checkedAt: "Checked at ",
|
||||
monitoringPaused: "Monitoring Paused",
|
||||
noAutomaticChecks: "No automatic checks",
|
||||
|
||||
//ServiveEditDialog.tsx
|
||||
editService: "Edit Service",
|
||||
editServiceDesc: "Update the details of your monitored service.",
|
||||
|
||||
//ServiceStatsCards.tsx
|
||||
lastCheckedAt: "Last checked at {datetime}",
|
||||
avg: "Avg",
|
||||
lastUpChecksCount: "last {count} up checks",
|
||||
basedOnlastChecksCount: "Based on last {count} checks",
|
||||
totalUptime: "Total Uptime",
|
||||
totalDowntime: "Total Downtime",
|
||||
monitoringSettings: "Monitoring Settings",
|
||||
monitoringSettingsInterval: "Checked every {interval} seconds",
|
||||
monitoringSettingsType: "monitoring",
|
||||
upStatusDuration: "Up for {duration}",
|
||||
downStatusDuration: "Down for {duration}",
|
||||
|
||||
incidentHistory: "Incident History",
|
||||
processing: "Processing",
|
||||
back: "Back",
|
||||
last20Checks: "Last 20 checks",
|
||||
};
|
||||
@@ -37,8 +37,12 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
selectCollection: "Select collection",
|
||||
toEmailAddress: "To email address",
|
||||
enterEmailAddress: "Enter email address",
|
||||
send: "Send",
|
||||
sending: "Sending...",
|
||||
|
||||
testEmailSettings: "Test Email Settings",
|
||||
testEmailDescription: "Test whether the current Email Settings are available",
|
||||
testEmailAlert: "This will send a test email using your configured SMTP settings. Make sure SMTP is properly configured first.",
|
||||
|
||||
// General Settings - Actions and status
|
||||
save: "Save Changes",
|
||||
saving: "Saving...",
|
||||
|
||||
@@ -14,4 +14,19 @@ export interface AboutTranslations {
|
||||
quickActionsDescription: string;
|
||||
quickTips: string;
|
||||
releasedOn: string;
|
||||
updateSchema: string;
|
||||
updateSchemaDesc: string;
|
||||
mergeFieldsLabel: string;
|
||||
importing: string;
|
||||
clickToUpdateSchema: string;
|
||||
importSuccessful: string;
|
||||
importFailed: string;
|
||||
instructions: string;
|
||||
mergeFields: string;
|
||||
instructionsMergeFields: string;
|
||||
instructionsCollections: string;
|
||||
instructionsImportAuth: string;
|
||||
collectionsUpdatedCount: string;
|
||||
collectionsCreatedCount: string;
|
||||
collectionsSkippedCount: string;
|
||||
}
|
||||
|
||||
@@ -59,4 +59,5 @@ export interface IncidentTranslations {
|
||||
affectedSystems: string;
|
||||
enterAffectedSystems: string;
|
||||
separateSystemsWithComma: string;
|
||||
createIncidentDesc: string;
|
||||
}
|
||||
|
||||
@@ -76,4 +76,12 @@ export interface MaintenanceTranslations {
|
||||
selectNotificationChannel: string;
|
||||
enableNotificationsFirst: string;
|
||||
updateMaintenance: string;
|
||||
loginToViewProfile: string;
|
||||
goToLogin: string;
|
||||
loadingUserData: string;
|
||||
retry: string;
|
||||
loadingServerData: string;
|
||||
retrievingYourInformation: string;
|
||||
servicesPagination: string;
|
||||
servicesPaginationNoService: string;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
export interface ServicesTranslations {
|
||||
|
||||
serviceStatus: string;
|
||||
responseTime: string;
|
||||
uptime: string;
|
||||
lastChecked: string;
|
||||
noServices: string;
|
||||
@@ -93,5 +92,46 @@ export interface ServicesTranslations {
|
||||
upServices: string;
|
||||
downServices: string;
|
||||
pausedServices: string;
|
||||
warningServices: string;
|
||||
warningServices: string;
|
||||
|
||||
// ServiceRowActions.tsx
|
||||
viewDetail: string;
|
||||
resumeMonitoring: string;
|
||||
pauseMonitoring: string;
|
||||
unmuteAlerts: string;
|
||||
muteAlerts: string;
|
||||
|
||||
//IncidentTable.tsx
|
||||
responseTime: string;
|
||||
errorMessage: string;
|
||||
details: string;
|
||||
|
||||
//LastCheckedTime.tsx
|
||||
pausedAt: string;
|
||||
lastCheckDetails: string;
|
||||
checkedAt: string;
|
||||
monitoringPaused: string;
|
||||
noAutomaticChecks: string;
|
||||
|
||||
//ServiveEditDialog.tsx
|
||||
editService: string;
|
||||
editServiceDesc: string;
|
||||
|
||||
//ServiceStatsCards.tsx
|
||||
lastCheckedAt: string;
|
||||
avg: string;
|
||||
lastUpChecksCount: string;
|
||||
basedOnlastChecksCount: string;
|
||||
totalUptime: string;
|
||||
totalDowntime: string;
|
||||
monitoringSettings: string;
|
||||
monitoringSettingsInterval: string;
|
||||
monitoringSettingsType: string;
|
||||
upStatusDuration: string;
|
||||
downStatusDuration: string;
|
||||
|
||||
incidentHistory: string;
|
||||
processing: string;
|
||||
back: string;
|
||||
last20Checks: string;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface SettingsTranslations {
|
||||
smtpAuthMethod: string;
|
||||
enableTLS: string;
|
||||
localName: string;
|
||||
|
||||
|
||||
// General Settings - Test Email
|
||||
testEmail: string;
|
||||
sendTestEmail: string;
|
||||
@@ -35,8 +35,12 @@ export interface SettingsTranslations {
|
||||
selectCollection: string;
|
||||
toEmailAddress: string;
|
||||
enterEmailAddress: string;
|
||||
send: string;
|
||||
sending: string;
|
||||
|
||||
testEmailSettings: string;
|
||||
testEmailDescription: string;
|
||||
testEmailAlert: string;
|
||||
|
||||
// General Settings - Actions and status
|
||||
save: string;
|
||||
saving: string;
|
||||
|
||||
@@ -16,4 +16,19 @@ export const aboutTranslations: AboutTranslations = {
|
||||
quickActionsDescription: "快速访问常用的监控操作和功能。选择下面的操作开始。",
|
||||
quickTips: "快速提示",
|
||||
releasedOn: "发布于",
|
||||
updateSchema: "更新架构",
|
||||
updateSchemaDesc: "自动导入集合架构",
|
||||
mergeFieldsLabel: "合并字段到现有集合(安全 - 保留数据)",
|
||||
importing: "导入中...",
|
||||
clickToUpdateSchema: "点击更新架构",
|
||||
importSuccessful: "导入成功",
|
||||
importFailed: "导入失败",
|
||||
instructions: "说明",
|
||||
mergeFields: "合并字段",
|
||||
instructionsMergeFields: "安全地向现有集合添加新字段,保留所有数据",
|
||||
instructionsCollections: "系统集合(以_开头的)和用户集合将被自动跳过",
|
||||
instructionsImportAuth: "只有经过身份验证的管理员才能执行架构导入",
|
||||
collectionsUpdatedCount: "已更新 {count} 个集合",
|
||||
collectionsCreatedCount: "已创建 {count} 个集合",
|
||||
collectionsSkippedCount: "已跳过 {count} 个集合",
|
||||
};
|
||||
|
||||
@@ -53,4 +53,13 @@ export const incidentTranslations: IncidentTranslations = {
|
||||
failedToUpdateStatus: '更新状态失败',
|
||||
inProgress: '进行中',
|
||||
enterRootCause: '输入故障根源',
|
||||
enterIncidentTitle: '输入事件标题',
|
||||
enterIncidentDescription: '输入事件描述',
|
||||
enterServiceId: '输入以逗号分隔的服务ID',
|
||||
selectAssignedUser: '选择分配用户',
|
||||
noAssignedUser: '该事件当前未分配给任何用户',
|
||||
affectedSystems: '受影响系统',
|
||||
enterAffectedSystems: '受影响系统',
|
||||
separateSystemsWithComma: '输入受影响的系统,以逗号分隔',
|
||||
createIncidentDesc: '创建一个新的事件',
|
||||
};
|
||||
|
||||
@@ -65,4 +65,25 @@ export const maintenanceTranslations: MaintenanceTranslations = {
|
||||
noScheduledMaintenance: '没有计划的维护',
|
||||
noMaintenanceWindows: '没有此时间段的维护窗口。通过点击“创建维护”按钮创建一个。',
|
||||
maintenanceCreatedSuccess: '维护窗口创建成功',
|
||||
editMaintenanceWindow: '编辑维护',
|
||||
editMaintenanceDesc: '编辑维护详情',
|
||||
priorityDescription: '选择维护优先级',
|
||||
statusDescription: '选择维护状态',
|
||||
impactLevel: '影响级别',
|
||||
impactLevelDescription: '选择影响级别',
|
||||
selectAssignedUsers: '选择分配的用户',
|
||||
assignedPersonnelDescription: '当前分配的用户',
|
||||
mutedNotifications: '通知已禁用',
|
||||
notificationsAreMuted: '此维护期间的通知当前已静音',
|
||||
selectNotificationChannel: '添加通知渠道',
|
||||
enableNotificationsFirst: '首先启用通知以选择频道',
|
||||
updateMaintenance: '更新维护',
|
||||
loginToViewProfile: '请登录查看您的个人资料',
|
||||
goToLogin: '立即登录',
|
||||
loadingUserData: '正在加载用户数据...',
|
||||
retry: '重试',
|
||||
loadingServerData: '正在加载服务器数据...',
|
||||
retrievingYourInformation: '正在获取您的信息,请稍候...',
|
||||
servicesPagination: '{startItem}-{endItem}, 共 {totalItems} 个服务',
|
||||
servicesPaginationNoService: '0 服务',
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ServicesTranslations } from '../types/services';
|
||||
|
||||
export const servicesTranslations: ServicesTranslations = {
|
||||
serviceStatus: "服务状态",
|
||||
responseTime: "响应时间",
|
||||
uptime: "在线时间",
|
||||
lastChecked: "最后检查时间",
|
||||
noServices: "没有符合您筛选条件的服务。",
|
||||
@@ -95,4 +94,45 @@ export const servicesTranslations: ServicesTranslations = {
|
||||
downServices: "下线服务",
|
||||
pausedServices: "暂停服务",
|
||||
warningServices: "告警服务",
|
||||
|
||||
// ServiceRowActions.tsx
|
||||
viewDetail: "查看详情",
|
||||
resumeMonitoring: "恢复监控",
|
||||
pauseMonitoring: "暂停监控",
|
||||
unmuteAlerts: "取消静音警报",
|
||||
muteAlerts: "静音警报",
|
||||
|
||||
//IncidentTable.tsx
|
||||
responseTime: "响应时间",
|
||||
errorMessage: "错误信息",
|
||||
details: "详情",
|
||||
|
||||
//LastCheckedTime.tsx
|
||||
pausedAt: "暂停于 ",
|
||||
lastCheckDetails: "最近检查详情",
|
||||
checkedAt: "检查于 ",
|
||||
monitoringPaused: "监控已暂停",
|
||||
noAutomaticChecks: "无自动检查",
|
||||
|
||||
//ServiveEditDialog.tsx
|
||||
editService: "编辑服务",
|
||||
editServiceDesc: "更新您所监控服务的详细信息。",
|
||||
|
||||
//ServiceStatsCards.tsx
|
||||
lastCheckedAt: "最新检查时间 {datetime}",
|
||||
avg: "平均",
|
||||
lastUpChecksCount: "最近 {count} 次检查",
|
||||
basedOnlastChecksCount: "基于最近的 {count} 次检查",
|
||||
totalUptime: "总在线时间",
|
||||
totalDowntime: "总宕机时间",
|
||||
monitoringSettings: "监控设置",
|
||||
monitoringSettingsInterval: "每 {interval} 秒检查一次",
|
||||
monitoringSettingsType: "监控",
|
||||
upStatusDuration: "已在线 {duration}",
|
||||
downStatusDuration: "已宕机 {duration}",
|
||||
|
||||
incidentHistory: "事件历史",
|
||||
processing: "处理中",
|
||||
back: "返回",
|
||||
last20Checks: "最近 20 次检查",
|
||||
};
|
||||
@@ -37,8 +37,12 @@ export const settingsTranslations: SettingsTranslations = {
|
||||
selectCollection: "选择集合",
|
||||
toEmailAddress: "收件人邮箱地址",
|
||||
enterEmailAddress: "输入收件人邮箱地址",
|
||||
send: "发送",
|
||||
sending: "发送中...",
|
||||
|
||||
testEmailSettings: "测试邮箱配置",
|
||||
testEmailDescription: "测试当前的邮箱设置是否可用",
|
||||
testEmailAlert: "这将使用您配置的SMTP设置发送一封测试邮件。请确保SMTP已正确配置。",
|
||||
|
||||
// General Settings - Actions and status
|
||||
save: "保存变更",
|
||||
saving: "保存中...",
|
||||
|
||||
Reference in New Issue
Block a user