mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-27 15:18:09 +00:00
i18n final
This commit is contained in:
parent
58dd396241
commit
3e4f7c3641
@ -85,6 +85,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface Application {
|
interface Application {
|
||||||
id: number;
|
id: number;
|
||||||
@ -112,6 +113,7 @@ interface ApplicationsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
const t = useTranslations();
|
||||||
const [name, setName] = useState<string>("");
|
const [name, setName] = useState<string>("");
|
||||||
const [description, setDescription] = useState<string>("");
|
const [description, setDescription] = useState<string>("");
|
||||||
const [icon, setIcon] = useState<string>("");
|
const [icon, setIcon] = useState<string>("");
|
||||||
@ -182,31 +184,28 @@ export default function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleItemsPerPageChange = (value: string) => {
|
const handleItemsPerPageChange = (value: string) => {
|
||||||
// Clear any existing timer
|
|
||||||
if (debounceTimerRef.current) {
|
if (debounceTimerRef.current) {
|
||||||
clearTimeout(debounceTimerRef.current);
|
clearTimeout(debounceTimerRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a new timer
|
|
||||||
debounceTimerRef.current = setTimeout(() => {
|
debounceTimerRef.current = setTimeout(() => {
|
||||||
const newItemsPerPage = parseInt(value);
|
const newItemsPerPage = parseInt(value);
|
||||||
|
|
||||||
// Ensure the value is within the valid range
|
|
||||||
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
||||||
toast.error("Please enter a number between 1 and 100");
|
toast.error(t('Applications.Messages.NumberValidation'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||||
|
|
||||||
setItemsPerPage(validatedValue);
|
setItemsPerPage(validatedValue);
|
||||||
setCurrentPage(1); // Reset to first page when changing items per page
|
setCurrentPage(1);
|
||||||
Cookies.set("itemsPerPage-app", String(validatedValue), {
|
Cookies.set("itemsPerPage-app", String(validatedValue), {
|
||||||
expires: 365,
|
expires: 365,
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
}, 300); // 300ms delay
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const add = async () => {
|
const add = async () => {
|
||||||
@ -221,10 +220,10 @@ export default function Dashboard() {
|
|||||||
uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "",
|
uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "",
|
||||||
});
|
});
|
||||||
getApplications();
|
getApplications();
|
||||||
toast.success("Application added successfully");
|
toast.success(t('Applications.Messages.AddSuccess'));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response?.data);
|
console.log(error.response?.data);
|
||||||
toast.error("Failed to add application");
|
toast.error(t('Applications.Messages.AddError'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,7 +243,7 @@ export default function Dashboard() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response?.data);
|
console.log(error.response?.data);
|
||||||
toast.error("Failed to get applications");
|
toast.error(t('Applications.Messages.GetError'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -265,10 +264,10 @@ export default function Dashboard() {
|
|||||||
try {
|
try {
|
||||||
await axios.post("/api/applications/delete", { id });
|
await axios.post("/api/applications/delete", { id });
|
||||||
getApplications();
|
getApplications();
|
||||||
toast.success("Application deleted successfully");
|
toast.success(t('Applications.Messages.DeleteSuccess'));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response?.data);
|
console.log(error.response?.data);
|
||||||
toast.error("Failed to delete application");
|
toast.error(t('Applications.Messages.DeleteError'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -306,10 +305,10 @@ export default function Dashboard() {
|
|||||||
});
|
});
|
||||||
getApplications();
|
getApplications();
|
||||||
setEditId(null);
|
setEditId(null);
|
||||||
toast.success("Application edited successfully");
|
toast.success(t('Applications.Messages.EditSuccess'));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error.response.data);
|
console.log(error.response.data);
|
||||||
toast.error("Failed to edit application");
|
toast.error(t('Applications.Messages.EditError'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -363,11 +362,11 @@ export default function Dashboard() {
|
|||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
|
<BreadcrumbPage>{t('Applications.Breadcrumb.MyInfrastructure')}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage>Applications</BreadcrumbPage>
|
<BreadcrumbPage>{t('Applications.Breadcrumb.Applications')}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
@ -376,11 +375,11 @@ export default function Dashboard() {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-3xl font-bold">Your Applications</span>
|
<span className="text-3xl font-bold">{t('Applications.Title')}</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon" title="Change view">
|
<Button variant="outline" size="icon" title={t('Applications.Views.ChangeView')}>
|
||||||
{isCompactLayout ? (
|
{isCompactLayout ? (
|
||||||
<Grid3X3 className="h-4 w-4" />
|
<Grid3X3 className="h-4 w-4" />
|
||||||
) : isGridLayout ? (
|
) : isGridLayout ? (
|
||||||
@ -392,13 +391,13 @@ export default function Dashboard() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => toggleLayout("standard")}>
|
<DropdownMenuItem onClick={() => toggleLayout("standard")}>
|
||||||
<List className="h-4 w-4 mr-2" /> List View
|
<List className="h-4 w-4 mr-2" /> {t('Applications.Views.ListView')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => toggleLayout("grid")}>
|
<DropdownMenuItem onClick={() => toggleLayout("grid")}>
|
||||||
<LayoutGrid className="h-4 w-4 mr-2" /> Grid View
|
<LayoutGrid className="h-4 w-4 mr-2" /> {t('Applications.Views.GridView')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => toggleLayout("compact")}>
|
<DropdownMenuItem onClick={() => toggleLayout("compact")}>
|
||||||
<Grid3X3 className="h-4 w-4 mr-2" /> Compact View
|
<Grid3X3 className="h-4 w-4 mr-2" /> {t('Applications.Views.CompactView')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@ -489,7 +488,7 @@ export default function Dashboard() {
|
|||||||
</Select>
|
</Select>
|
||||||
{servers.length === 0 ? (
|
{servers.length === 0 ? (
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
You must first add a server.
|
{t('Applications.Messages.AddServerFirst')}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@ -500,24 +499,24 @@ export default function Dashboard() {
|
|||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
|
<AlertDialogContent className="max-w-[90vw] w-[600px] max-h-[90vh] overflow-y-auto">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Add an application</AlertDialogTitle>
|
<AlertDialogTitle>{t('Applications.Add.Title')}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
<div className="space-y-4 pt-4">
|
<div className="space-y-4 pt-4">
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label>Name</Label>
|
<Label>{t('Applications.Add.Name')}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g. Portainer"
|
placeholder={t('Applications.Add.NamePlaceholder')}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label>Server</Label>
|
<Label>{t('Applications.Add.Server')}</Label>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(v) => setServerId(Number(v))}
|
onValueChange={(v) => setServerId(Number(v))}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select server" />
|
<SelectValue placeholder={t('Applications.Add.SelectServer')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{servers.map((server) => (
|
{servers.map((server) => (
|
||||||
@ -533,53 +532,35 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label>
|
<Label>
|
||||||
Description{" "}
|
{t('Applications.Add.Description')}{" "}
|
||||||
<span className="text-stone-600">(optional)</span>
|
<span className="text-stone-600">{t('Common.optional')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Application description"
|
placeholder={t('Applications.Add.DescriptionPlaceholder')}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label>
|
<Label>{t('Applications.Add.IconURL')}</Label>
|
||||||
Icon URL{" "}
|
<Input
|
||||||
<span className="text-stone-600">(optional)</span>
|
placeholder={t('Applications.Add.IconURLPlaceholder')}
|
||||||
</Label>
|
onChange={(e) => setIcon(e.target.value)}
|
||||||
<div className="flex gap-2">
|
/>
|
||||||
<Input
|
|
||||||
value={icon}
|
|
||||||
placeholder="https://example.com/icon.png"
|
|
||||||
onChange={(e) => setIcon(e.target.value)}
|
|
||||||
/>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button variant="outline" size="icon" onClick={generateIconURL}>
|
|
||||||
<Zap />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
Generate Icon URL
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label>Public URL</Label>
|
<Label>{t('Applications.Add.PublicURL')}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://example.com"
|
placeholder={t('Applications.Add.PublicURLPlaceholder')}
|
||||||
onChange={(e) => setPublicURL(e.target.value)}
|
onChange={(e) => setPublicURL(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label>
|
<Label>
|
||||||
Local URL{" "}
|
{t('Applications.Add.LocalURL')}{" "}
|
||||||
<span className="text-stone-600">(optional)</span>
|
<span className="text-stone-600">{t('Common.optional')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="http://localhost:3000"
|
placeholder={t('Applications.Add.LocalURLPlaceholder')}
|
||||||
onChange={(e) => setLocalURL(e.target.value)}
|
onChange={(e) => setLocalURL(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -591,23 +572,23 @@ export default function Dashboard() {
|
|||||||
onChange={(e) => setCustomUptimeCheck(e.target.checked)}
|
onChange={(e) => setCustomUptimeCheck(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="custom-uptime-check">Custom Uptime Check URL</Label>
|
<Label htmlFor="custom-uptime-check">{t('Applications.Add.CustomUptimeCheck')}</Label>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
When enabled, this URL replaces the Public URL for uptime monitoring checks
|
{t('Applications.Add.CustomUptimeCheckTooltip')}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
{customUptimeCheck && (
|
{customUptimeCheck && (
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label>Uptime Check URL</Label>
|
<Label>{t('Applications.Add.UptimeCheckURL')}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://example.com/status"
|
placeholder={t('Applications.Add.UptimeCheckURLPlaceholder')}
|
||||||
value={uptimecheckUrl}
|
value={uptimecheckUrl}
|
||||||
onChange={(e) => setUptimecheckUrl(e.target.value)}
|
onChange={(e) => setUptimecheckUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -617,12 +598,12 @@ export default function Dashboard() {
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={add}
|
onClick={add}
|
||||||
disabled={!name || !publicURL || !serverId}
|
disabled={!name || !publicURL || !serverId}
|
||||||
>
|
>
|
||||||
Add
|
{t('Common.add')}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
@ -633,7 +614,7 @@ export default function Dashboard() {
|
|||||||
<div className="flex flex-col gap-2 mb-4 pt-2">
|
<div className="flex flex-col gap-2 mb-4 pt-2">
|
||||||
<Input
|
<Input
|
||||||
id="application-search"
|
id="application-search"
|
||||||
placeholder="Type to search..."
|
placeholder={t('Applications.Search.Placeholder')}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -668,7 +649,7 @@ export default function Dashboard() {
|
|||||||
className="w-full h-full object-contain rounded-md"
|
className="w-full h-full object-contain rounded-md"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500 text-xs">Icon</span>
|
<span className="text-gray-500 text-xs">{t('Applications.Card.Icon')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
@ -698,7 +679,7 @@ export default function Dashboard() {
|
|||||||
className="w-full h-full object-contain rounded-md"
|
className="w-full h-full object-contain rounded-md"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500 text-xs">Image</span>
|
<span className="text-gray-500 text-xs">{t('Applications.Card.Image')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
@ -710,7 +691,7 @@ export default function Dashboard() {
|
|||||||
{app.description && (
|
{app.description && (
|
||||||
<br className="hidden md:block" />
|
<br className="hidden md:block" />
|
||||||
)}
|
)}
|
||||||
Server: {app.server || "No server"}
|
{t('Applications.Card.Server')}: {app.server || t('Applications.Card.NoServer')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -727,7 +708,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link className="h-4 w-4" />
|
<Link className="h-4 w-4" />
|
||||||
Public URL
|
{t('Applications.Card.PublicURL')}
|
||||||
</Button>
|
</Button>
|
||||||
{app.localURL && (
|
{app.localURL && (
|
||||||
<Button
|
<Button
|
||||||
@ -738,7 +719,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Home className="h-4 w-4" />
|
<Home className="h-4 w-4" />
|
||||||
Local URL
|
{t('Applications.Card.LocalURL')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -929,7 +910,7 @@ export default function Dashboard() {
|
|||||||
onClick={() => window.open(app.publicURL, "_blank")}
|
onClick={() => window.open(app.publicURL, "_blank")}
|
||||||
>
|
>
|
||||||
<Link className="h-4 w-4" />
|
<Link className="h-4 w-4" />
|
||||||
Public URL
|
{t('Applications.Card.PublicURL')}
|
||||||
</Button>
|
</Button>
|
||||||
{app.localURL && (
|
{app.localURL && (
|
||||||
<Button
|
<Button
|
||||||
@ -938,7 +919,7 @@ export default function Dashboard() {
|
|||||||
onClick={() => window.open(app.localURL, "_blank")}
|
onClick={() => window.open(app.localURL, "_blank")}
|
||||||
>
|
>
|
||||||
<Home className="h-4 w-4" />
|
<Home className="h-4 w-4" />
|
||||||
Local URL
|
{t('Applications.Card.LocalURL')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1124,7 +1105,7 @@ export default function Dashboard() {
|
|||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<div className="inline-block" role="status" aria-label="loading">
|
<div className="inline-block" role="status" aria-label="loading">
|
||||||
<svg
|
<svg
|
||||||
className="w-6 h-6 stroke-white animate-spin "
|
className="w-6 h-6 stroke-white animate-spin"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -1144,14 +1125,16 @@ export default function Dashboard() {
|
|||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="sr-only">Loading...</span>
|
<span className="sr-only">{t('Common.Loading')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="pt-4 pb-4">
|
<div className="pt-4 pb-4">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{totalItems > 0 ? `Showing ${startItem}-${endItem} of ${totalItems} applications` : "No applications found"}
|
{totalItems > 0
|
||||||
|
? t('Applications.Pagination.Showing', { start: startItem, end: endItem, total: totalItems })
|
||||||
|
: t('Applications.Pagination.NoApplications')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Pagination>
|
<Pagination>
|
||||||
|
|||||||
@ -16,8 +16,10 @@ import {
|
|||||||
import { ReactFlow, Controls, Background, ConnectionLineType } from "@xyflow/react";
|
import { ReactFlow, Controls, Background, ConnectionLineType } from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
const t = useTranslations();
|
||||||
const [nodes, setNodes] = useState<any[]>([]);
|
const [nodes, setNodes] = useState<any[]>([]);
|
||||||
const [edges, setEdges] = useState<any[]>([]);
|
const [edges, setEdges] = useState<any[]>([]);
|
||||||
|
|
||||||
@ -58,13 +60,13 @@ export default function Dashboard() {
|
|||||||
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
|
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage className="dark:text-slate-300">
|
<BreadcrumbPage className="dark:text-slate-300">
|
||||||
My Infrastructure
|
{t('Network.Breadcrumb.MyInfrastructure')}
|
||||||
</BreadcrumbPage>
|
</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
|
<BreadcrumbSeparator className="hidden md:block dark:text-slate-500" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage className="dark:text-slate-300">
|
<BreadcrumbPage className="dark:text-slate-300">
|
||||||
Network
|
{t('Network.Breadcrumb.Network')}
|
||||||
</BreadcrumbPage>
|
</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import {
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
|
||||||
interface NotificationsResponse {
|
interface NotificationsResponse {
|
||||||
notifications: any[]
|
notifications: any[]
|
||||||
@ -46,6 +47,7 @@ interface NotificationResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
|
const t = useTranslations()
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
const [email, setEmail] = useState<string>("")
|
const [email, setEmail] = useState<string>("")
|
||||||
@ -92,7 +94,7 @@ export default function Settings() {
|
|||||||
setEmailError("")
|
setEmailError("")
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
setEmailError("Email is required")
|
setEmailError(t('Settings.UserSettings.ChangeEmail.EmailRequired'))
|
||||||
setEmailErrorVisible(true)
|
setEmailErrorVisible(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setEmailErrorVisible(false)
|
setEmailErrorVisible(false)
|
||||||
@ -123,7 +125,7 @@ export default function Settings() {
|
|||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
try {
|
try {
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
setPasswordError("Passwords do not match")
|
setPasswordError(t('Settings.UserSettings.ChangePassword.PasswordsDontMatch'))
|
||||||
setPasswordErrorVisible(true)
|
setPasswordErrorVisible(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPasswordErrorVisible(false)
|
setPasswordErrorVisible(false)
|
||||||
@ -132,7 +134,7 @@ export default function Settings() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!oldPassword || !password || !confirmPassword) {
|
if (!oldPassword || !password || !confirmPassword) {
|
||||||
setPasswordError("All fields are required")
|
setPasswordError(t('Settings.UserSettings.ChangePassword.AllFieldsRequired'))
|
||||||
setPasswordErrorVisible(true)
|
setPasswordErrorVisible(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPasswordErrorVisible(false)
|
setPasswordErrorVisible(false)
|
||||||
@ -263,7 +265,7 @@ export default function Settings() {
|
|||||||
const response = await axios.post("/api/notifications/test", {
|
const response = await axios.post("/api/notifications/test", {
|
||||||
notificationId: id,
|
notificationId: id,
|
||||||
})
|
})
|
||||||
toast.success("Notification will be sent in a few seconds.")
|
toast.success(t('Settings.Notifications.TestSuccess'))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.response.data.error)
|
toast.error(error.response.data.error)
|
||||||
}
|
}
|
||||||
@ -280,15 +282,11 @@ export default function Settings() {
|
|||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem className="hidden md:block">
|
<BreadcrumbItem className="hidden md:block">
|
||||||
<BreadcrumbPage>/</BreadcrumbPage>
|
<BreadcrumbPage>{t('Settings.Breadcrumb.Dashboard')}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage>Dashboard</BreadcrumbPage>
|
<BreadcrumbPage>{t('Settings.Breadcrumb.Settings')}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>Settings</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
@ -296,31 +294,31 @@ export default function Settings() {
|
|||||||
</header>
|
</header>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
<span className="text-3xl font-bold">Settings</span>
|
<span className="text-3xl font-bold">{t('Settings.Title')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
|
||||||
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="h-5 w-5 text-primary" />
|
<User className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-xl font-semibold">User Settings</h2>
|
<h2 className="text-xl font-semibold">{t('Settings.UserSettings.Title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pb-6">
|
<CardContent className="pb-6">
|
||||||
<div className="text-sm text-muted-foreground mb-6">
|
<div className="text-sm text-muted-foreground mb-6">
|
||||||
Manage your user settings here. You can change your email, password, and other account settings.
|
{t('Settings.UserSettings.Description')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-8">
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="border-b pb-2">
|
<div className="border-b pb-2">
|
||||||
<h3 className="font-semibold text-lg">Change Email</h3>
|
<h3 className="font-semibold text-lg">{t('Settings.UserSettings.ChangeEmail.Title')}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{emailErrorVisible && (
|
{emailErrorVisible && (
|
||||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>{t('Common.Error')}</AlertTitle>
|
||||||
<AlertDescription>{emailError}</AlertDescription>
|
<AlertDescription>{emailError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@ -328,34 +326,33 @@ export default function Settings() {
|
|||||||
{emailSuccess && (
|
{emailSuccess && (
|
||||||
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
<AlertTitle>Success</AlertTitle>
|
<AlertTitle>{t('Settings.UserSettings.ChangeEmail.Success')}</AlertTitle>
|
||||||
<AlertDescription>Email changed successfully.</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Enter new email"
|
placeholder={t('Settings.UserSettings.ChangeEmail.Placeholder')}
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="h-11"
|
className="h-11"
|
||||||
/>
|
/>
|
||||||
<Button onClick={changeEmail} className="w-full h-11">
|
<Button onClick={changeEmail} className="w-full h-11">
|
||||||
Change Email
|
{t('Settings.UserSettings.ChangeEmail.Button')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="border-b pb-2">
|
<div className="border-b pb-2">
|
||||||
<h3 className="font-semibold text-lg">Change Password</h3>
|
<h3 className="font-semibold text-lg">{t('Settings.UserSettings.ChangePassword.Title')}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{passwordErrorVisible && (
|
{passwordErrorVisible && (
|
||||||
<Alert variant="destructive" className="animate-in fade-in-50">
|
<Alert variant="destructive" className="animate-in fade-in-50">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>{t('Common.Error')}</AlertTitle>
|
||||||
<AlertDescription>{passwordError}</AlertDescription>
|
<AlertDescription>{passwordError}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@ -363,35 +360,34 @@ export default function Settings() {
|
|||||||
{passwordSuccess && (
|
{passwordSuccess && (
|
||||||
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
<Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-300 animate-in fade-in-50">
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
<AlertTitle>Success</AlertTitle>
|
<AlertTitle>{t('Settings.UserSettings.ChangePassword.Success')}</AlertTitle>
|
||||||
<AlertDescription>Password changed successfully.</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter old password"
|
placeholder={t('Settings.UserSettings.ChangePassword.OldPassword')}
|
||||||
value={oldPassword}
|
value={oldPassword}
|
||||||
onChange={(e) => setOldPassword(e.target.value)}
|
onChange={(e) => setOldPassword(e.target.value)}
|
||||||
className="h-11"
|
className="h-11"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter new password"
|
placeholder={t('Settings.UserSettings.ChangePassword.NewPassword')}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="h-11"
|
className="h-11"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Confirm new password"
|
placeholder={t('Settings.UserSettings.ChangePassword.ConfirmPassword')}
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
className="h-11"
|
className="h-11"
|
||||||
/>
|
/>
|
||||||
<Button onClick={changePassword} className="w-full h-11">
|
<Button onClick={changePassword} className="w-full h-11">
|
||||||
Change Password
|
{t('Settings.UserSettings.ChangePassword.Button')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -403,25 +399,25 @@ export default function Settings() {
|
|||||||
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Palette className="h-5 w-5 text-primary" />
|
<Palette className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-xl font-semibold">Theme Settings</h2>
|
<h2 className="text-xl font-semibold">{t('Settings.ThemeSettings.Title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pb-6">
|
<CardContent className="pb-6">
|
||||||
<div className="text-sm text-muted-foreground mb-6">
|
<div className="text-sm text-muted-foreground mb-6">
|
||||||
Select a theme for the application. You can choose between light, dark, or system theme.
|
{t('Settings.ThemeSettings.Description')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<Select value={theme} onValueChange={(value: string) => setTheme(value)}>
|
<Select value={theme} onValueChange={(value: string) => setTheme(value)}>
|
||||||
<SelectTrigger className="w-full h-11">
|
<SelectTrigger className="w-full h-11">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{(theme ?? "system").charAt(0).toUpperCase() + (theme ?? "system").slice(1)}
|
{t(`Settings.ThemeSettings.${(theme ?? "system").charAt(0).toUpperCase() + (theme ?? "system").slice(1)}`)}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="light">Light</SelectItem>
|
<SelectItem value="light">{t('Settings.ThemeSettings.Light')}</SelectItem>
|
||||||
<SelectItem value="dark">Dark</SelectItem>
|
<SelectItem value="dark">{t('Settings.ThemeSettings.Dark')}</SelectItem>
|
||||||
<SelectItem value="system">System</SelectItem>
|
<SelectItem value="system">{t('Settings.ThemeSettings.System')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@ -434,61 +430,59 @@ export default function Settings() {
|
|||||||
<div className="bg-muted/20 p-2 rounded-full">
|
<div className="bg-muted/20 p-2 rounded-full">
|
||||||
<Bell className="h-5 w-5 text-primary" />
|
<Bell className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-semibold">Notifications</h2>
|
<h2 className="text-xl font-semibold">{t('Settings.Notifications.Title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="text-sm text-muted-foreground mb-6">
|
<div className="text-sm text-muted-foreground mb-6">
|
||||||
Set up notifications to get instantly alerted when an application changes status.
|
{t('Settings.Notifications.Description')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button className="w-full h-11 flex items-center gap-2">
|
<Button className="w-full h-11 flex items-center gap-2">
|
||||||
Add Notification Channel
|
{t('Settings.Notifications.AddChannel')}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogTitle>Add Notification</AlertDialogTitle>
|
<AlertDialogTitle>{t('Settings.Notifications.AddNotification.Title')}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="notificationName"
|
id="notificationName"
|
||||||
placeholder="Notification Name (optional)"
|
placeholder={t('Settings.Notifications.AddNotification.Name')}
|
||||||
onChange={(e) => setNotificationName(e.target.value)}
|
onChange={(e) => setNotificationName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
|
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Notification Type" />
|
<SelectValue placeholder={t('Settings.Notifications.AddNotification.Type')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="smtp">SMTP</SelectItem>
|
<SelectItem value="smtp">{t('Settings.Notifications.AddNotification.SMTP.Title')}</SelectItem>
|
||||||
<SelectItem value="telegram">Telegram</SelectItem>
|
<SelectItem value="telegram">{t('Settings.Notifications.AddNotification.Telegram.Title')}</SelectItem>
|
||||||
<SelectItem value="discord">Discord</SelectItem>
|
<SelectItem value="discord">{t('Settings.Notifications.AddNotification.Discord.Title')}</SelectItem>
|
||||||
<SelectItem value="gotify">Gotify</SelectItem>
|
<SelectItem value="gotify">{t('Settings.Notifications.AddNotification.Gotify.Title')}</SelectItem>
|
||||||
<SelectItem value="ntfy">Ntfy</SelectItem>
|
<SelectItem value="ntfy">{t('Settings.Notifications.AddNotification.Ntfy.Title')}</SelectItem>
|
||||||
<SelectItem value="pushover">Pushover</SelectItem>
|
<SelectItem value="pushover">{t('Settings.Notifications.AddNotification.Pushover.Title')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
||||||
{notificationType === "smtp" && (
|
{notificationType === "smtp" && (
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="smtpHost">SMTP Host</Label>
|
<Label>{t('Settings.Notifications.AddNotification.SMTP.Host')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="smtpHost"
|
|
||||||
placeholder="smtp.example.com"
|
placeholder="smtp.example.com"
|
||||||
onChange={(e) => setSmtpHost(e.target.value)}
|
onChange={(e) => setSmtpHost(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="smtpPort">SMTP Port</Label>
|
<Label>{t('Settings.Notifications.AddNotification.SMTP.Port')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
id="smtpPort"
|
|
||||||
placeholder="587"
|
placeholder="587"
|
||||||
onChange={(e) => setSmtpPort(Number(e.target.value))}
|
onChange={(e) => setSmtpPort(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
@ -498,26 +492,24 @@ export default function Settings() {
|
|||||||
<div className="flex items-center space-x-2 pt-2 pb-4">
|
<div className="flex items-center space-x-2 pt-2 pb-4">
|
||||||
<Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} />
|
<Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} />
|
||||||
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
|
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
|
||||||
Secure Connection (TLS/SSL)
|
{t('Settings.Notifications.AddNotification.SMTP.Secure')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="smtpUser">SMTP Username</Label>
|
<Label>{t('Settings.Notifications.AddNotification.SMTP.User')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="smtpUser"
|
|
||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
onChange={(e) => setSmtpUsername(e.target.value)}
|
onChange={(e) => setSmtpUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="smtpPass">SMTP Password</Label>
|
<Label>{t('Settings.Notifications.AddNotification.SMTP.Pass')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
id="smtpPass"
|
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
onChange={(e) => setSmtpPassword(e.target.value)}
|
onChange={(e) => setSmtpPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -525,20 +517,18 @@ export default function Settings() {
|
|||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="smtpFrom">From Address</Label>
|
<Label>{t('Settings.Notifications.AddNotification.SMTP.From')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
id="smtpFrom"
|
|
||||||
placeholder="noreply@example.com"
|
placeholder="noreply@example.com"
|
||||||
onChange={(e) => setSmtpFrom(e.target.value)}
|
onChange={(e) => setSmtpFrom(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="smtpTo">To Address</Label>
|
<Label>{t('Settings.Notifications.AddNotification.SMTP.To')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
id="smtpTo"
|
|
||||||
placeholder="admin@example.com"
|
placeholder="admin@example.com"
|
||||||
onChange={(e) => setSmtpTo(e.target.value)}
|
onChange={(e) => setSmtpTo(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -551,20 +541,16 @@ export default function Settings() {
|
|||||||
{notificationType === "telegram" && (
|
{notificationType === "telegram" && (
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="telegramToken">Bot Token</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Telegram.Token')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="telegramToken"
|
|
||||||
placeholder=""
|
|
||||||
onChange={(e) => setTelegramToken(e.target.value)}
|
onChange={(e) => setTelegramToken(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="telegramChatId">Chat ID</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Telegram.ChatId')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="telegramChatId"
|
|
||||||
placeholder=""
|
|
||||||
onChange={(e) => setTelegramChatId(e.target.value)}
|
onChange={(e) => setTelegramChatId(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -574,11 +560,9 @@ export default function Settings() {
|
|||||||
{notificationType === "discord" && (
|
{notificationType === "discord" && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="discordWebhook">Webhook URL</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Discord.Webhook')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="discordWebhook"
|
|
||||||
placeholder=""
|
|
||||||
onChange={(e) => setDiscordWebhook(e.target.value)}
|
onChange={(e) => setDiscordWebhook(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -588,19 +572,15 @@ export default function Settings() {
|
|||||||
{notificationType === "gotify" && (
|
{notificationType === "gotify" && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="gotifyUrl">Gotify URL</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Gotify.Url')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="gotifyUrl"
|
|
||||||
placeholder=""
|
|
||||||
onChange={(e) => setGotifyUrl(e.target.value)}
|
onChange={(e) => setGotifyUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="gotifyToken">Gotify Token</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Gotify.Token')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="gotifyToken"
|
|
||||||
placeholder=""
|
|
||||||
onChange={(e) => setGotifyToken(e.target.value)}
|
onChange={(e) => setGotifyToken(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -611,19 +591,15 @@ export default function Settings() {
|
|||||||
{notificationType === "ntfy" && (
|
{notificationType === "ntfy" && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="ntfyUrl">Ntfy URL</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Ntfy.Url')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="ntfyUrl"
|
|
||||||
placeholder=""
|
|
||||||
onChange={(e) => setNtfyUrl(e.target.value)}
|
onChange={(e) => setNtfyUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="ntfyToken">Ntfy Token</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Ntfy.Token')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="ntfyToken"
|
|
||||||
placeholder=""
|
|
||||||
onChange={(e) => setNtfyToken(e.target.value)}
|
onChange={(e) => setNtfyToken(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -634,31 +610,28 @@ export default function Settings() {
|
|||||||
{notificationType === "pushover" && (
|
{notificationType === "pushover" && (
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="pushoverUrl">Pushover URL</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Pushover.Url')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="pushoverUrl"
|
placeholder={t('Settings.Notifications.AddNotification.Pushover.UrlPlaceholder')}
|
||||||
placeholder="e.g. https://api.pushover.net/1/messages.json"
|
|
||||||
onChange={(e) => setPushoverUrl(e.target.value)}
|
onChange={(e) => setPushoverUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="pushoverToken">Pushover Token</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Pushover.Token')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="pushoverToken"
|
placeholder={t('Settings.Notifications.AddNotification.Pushover.TokenPlaceholder')}
|
||||||
placeholder="e.g. 1234567890"
|
|
||||||
onChange={(e) => setPushoverToken(e.target.value)}
|
onChange={(e) => setPushoverToken(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid w-full items-center gap-1.5">
|
<div className="grid w-full items-center gap-1.5">
|
||||||
<Label htmlFor="pushoverUser">Pushover User</Label>
|
<Label>{t('Settings.Notifications.AddNotification.Pushover.User')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="pushoverUser"
|
placeholder={t('Settings.Notifications.AddNotification.Pushover.UserPlaceholder')}
|
||||||
placeholder="e.g. 1234567890"
|
|
||||||
onChange={(e) => setPushoverUser(e.target.value)}
|
onChange={(e) => setPushoverUser(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -668,8 +641,8 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={addNotification}>Add</AlertDialogAction>
|
<AlertDialogAction onClick={addNotification}>{t('Common.add')}</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
@ -677,65 +650,63 @@ export default function Settings() {
|
|||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button className="w-full h-11" variant="outline">
|
<Button className="w-full h-11" variant="outline">
|
||||||
Customize Notification Text
|
{t('Settings.Notifications.CustomizeText.Display')}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
|
<AlertDialogTitle>{t('Settings.Notifications.CustomizeText.Title')}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="text_application">Notification Text for Applications</Label>
|
<Label>{t('Settings.Notifications.CustomizeText.Application')}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="text_application"
|
|
||||||
placeholder="Type here..."
|
|
||||||
value={notificationTextApplication}
|
value={notificationTextApplication}
|
||||||
onChange={(e) => setNotificationTextApplication(e.target.value)}
|
onChange={(e) => setNotificationTextApplication(e.target.value)}
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="text_server">Notification Text for Servers</Label>
|
<Label>{t('Settings.Notifications.CustomizeText.Server')}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="text_server"
|
|
||||||
placeholder="Type here..."
|
|
||||||
value={notificationTextServer}
|
value={notificationTextServer}
|
||||||
onChange={(e) => setNotificationTextServer(e.target.value)}
|
onChange={(e) => setNotificationTextServer(e.target.value)}
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="pt-4 text-sm text-muted-foreground">
|
||||||
<div className="pt-4 text-sm text-muted-foreground">
|
{t('Settings.Notifications.CustomizeText.Placeholders.Title')}
|
||||||
You can use the following placeholders in the text:
|
<ul className="list-disc list-inside space-y-1 pt-2">
|
||||||
<ul className="list-disc list-inside space-y-1 pt-2">
|
<li>
|
||||||
<li>
|
<b>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Title')}</b>
|
||||||
<b>Server related:</b>
|
|
||||||
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
|
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
|
||||||
<li>!name - The name of the server</li>
|
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Name')}</li>
|
||||||
<li>!status - The current status of the server (online/offline)</li>
|
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Server.Status')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>Application related:</b>
|
<b>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Title')}</b>
|
||||||
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
|
<ul className="list-disc list-inside ml-4 space-y-1 pt-1 text-muted-foreground">
|
||||||
<li>!name - The name of the application</li>
|
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Name')}</li>
|
||||||
<li>!url - The URL where the application is hosted</li>
|
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Url')}</li>
|
||||||
<li>!status - The current status of the application (online/offline)</li>
|
<li>{t('Settings.Notifications.CustomizeText.Placeholders.Application.Status')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>{t('Common.cancel')}</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction>
|
<AlertDialogAction onClick={editNotificationText}>
|
||||||
|
{t('Common.Save')}
|
||||||
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<h3 className="text-lg font-medium mb-4">Active Notification Channels</h3>
|
<h3 className="text-lg font-medium mb-4">{t('Settings.Notifications.ActiveChannels')}</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{notifications.length > 0 ? (
|
{notifications.length > 0 ? (
|
||||||
notifications.map((notification) => (
|
notifications.map((notification) => (
|
||||||
@ -775,14 +746,12 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="font-medium capitalize">{notification.name && notification.name !== "" ? notification.name : notification.type}</h3>
|
<h3 className="font-medium capitalize">
|
||||||
|
{notification.name ||
|
||||||
|
t(`Settings.Notifications.AddNotification.${notification.type.charAt(0).toUpperCase() + notification.type.slice(1)}.Title`)}
|
||||||
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{notification.type === "smtp" && "Email notifications"}
|
{t(`Settings.Notifications.AddNotification.${notification.type.charAt(0).toUpperCase() + notification.type.slice(1)}.Description`)}
|
||||||
{notification.type === "telegram" && "Telegram bot alerts"}
|
|
||||||
{notification.type === "discord" && "Discord webhook alerts"}
|
|
||||||
{notification.type === "gotify" && "Gotify notifications"}
|
|
||||||
{notification.type === "ntfy" && "Ntfy notifications"}
|
|
||||||
{notification.type === "pushover" && "Pushover notifications"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -794,7 +763,7 @@ export default function Settings() {
|
|||||||
onClick={() => testNotification(notification.id)}
|
onClick={() => testNotification(notification.id)}
|
||||||
>
|
>
|
||||||
<Play className="h-4 w-4 mr-1" />
|
<Play className="h-4 w-4 mr-1" />
|
||||||
Test
|
{t('Settings.Notifications.Test')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -803,6 +772,7 @@ export default function Settings() {
|
|||||||
onClick={() => deleteNotification(notification.id)}
|
onClick={() => deleteNotification(notification.id)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
{t('Settings.Notifications.Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -814,9 +784,11 @@ export default function Settings() {
|
|||||||
<Bell className="h-6 w-6 text-muted-foreground" />
|
<Bell className="h-6 w-6 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium mb-1">No notifications configured</h3>
|
<h3 className="text-lg font-medium mb-1">
|
||||||
|
{t('Settings.Notifications.NoNotifications')}
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
||||||
Add a notification channel to get alerted when your applications change status.
|
{t('Settings.Notifications.NoNotificationsDescription')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -830,4 +802,4 @@ export default function Settings() {
|
|||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -31,6 +31,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
const timeFormats = {
|
const timeFormats = {
|
||||||
1: (timestamp: string) =>
|
1: (timestamp: string) =>
|
||||||
@ -81,6 +82,7 @@ interface PaginationData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Uptime() {
|
export default function Uptime() {
|
||||||
|
const t = useTranslations();
|
||||||
const [data, setData] = useState<UptimeData[]>([]);
|
const [data, setData] = useState<UptimeData[]>([]);
|
||||||
const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1);
|
const [timespan, setTimespan] = useState<1 | 2 | 3 | 4>(1);
|
||||||
const [pagination, setPagination] = useState<PaginationData>({
|
const [pagination, setPagination] = useState<PaginationData>({
|
||||||
@ -138,34 +140,30 @@ export default function Uptime() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleItemsPerPageChange = (value: string) => {
|
const handleItemsPerPageChange = (value: string) => {
|
||||||
// Clear any existing timer
|
|
||||||
if (debounceTimerRef.current) {
|
if (debounceTimerRef.current) {
|
||||||
clearTimeout(debounceTimerRef.current);
|
clearTimeout(debounceTimerRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a new timer
|
|
||||||
debounceTimerRef.current = setTimeout(() => {
|
debounceTimerRef.current = setTimeout(() => {
|
||||||
const newItemsPerPage = parseInt(value);
|
const newItemsPerPage = parseInt(value);
|
||||||
|
|
||||||
// Ensure the value is within the valid range
|
|
||||||
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
if (isNaN(newItemsPerPage) || newItemsPerPage < 1) {
|
||||||
toast.error("Please enter a number between 1 and 100");
|
toast.error(t('Uptime.Messages.NumberValidation'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100);
|
||||||
|
|
||||||
setItemsPerPage(validatedValue);
|
setItemsPerPage(validatedValue);
|
||||||
setPagination(prev => ({...prev, currentPage: 1})); // Reset to first page
|
setPagination(prev => ({...prev, currentPage: 1}));
|
||||||
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
|
Cookies.set("itemsPerPage-uptime", String(validatedValue), {
|
||||||
expires: 365,
|
expires: 365,
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch data with new pagination
|
|
||||||
getData(timespan, 1, validatedValue);
|
getData(timespan, 1, validatedValue);
|
||||||
}, 300); // 300ms delay
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -187,11 +185,11 @@ export default function Uptime() {
|
|||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
|
<BreadcrumbPage>{t('Uptime.Breadcrumb.MyInfrastructure')}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage>Uptime</BreadcrumbPage>
|
<BreadcrumbPage>{t('Uptime.Breadcrumb.Uptime')}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
@ -200,7 +198,7 @@ export default function Uptime() {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-3xl font-bold">Uptime</span>
|
<span className="text-3xl font-bold">{t('Uptime.Title')}</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={String(itemsPerPage)}
|
value={String(itemsPerPage)}
|
||||||
@ -213,22 +211,22 @@ export default function Uptime() {
|
|||||||
>
|
>
|
||||||
<SelectTrigger className="w-[140px]">
|
<SelectTrigger className="w-[140px]">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'}
|
{itemsPerPage} {itemsPerPage === 1 ? t('Common.ItemsPerPage.item') : t('Common.ItemsPerPage.items')}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
{![5, 10, 15, 20, 25].includes(itemsPerPage) ? (
|
||||||
<SelectItem value={String(itemsPerPage)}>
|
<SelectItem value={String(itemsPerPage)}>
|
||||||
{itemsPerPage} {itemsPerPage === 1 ? 'item' : 'items'} (custom)
|
{itemsPerPage} {itemsPerPage === 1 ? t('Common.ItemsPerPage.item') : t('Common.ItemsPerPage.items')} (custom)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : null}
|
) : null}
|
||||||
<SelectItem value="5">5 items</SelectItem>
|
<SelectItem value="5">{t('Common.ItemsPerPage.5')}</SelectItem>
|
||||||
<SelectItem value="10">10 items</SelectItem>
|
<SelectItem value="10">{t('Common.ItemsPerPage.10')}</SelectItem>
|
||||||
<SelectItem value="15">15 items</SelectItem>
|
<SelectItem value="15">{t('Common.ItemsPerPage.15')}</SelectItem>
|
||||||
<SelectItem value="20">20 items</SelectItem>
|
<SelectItem value="20">{t('Common.ItemsPerPage.20')}</SelectItem>
|
||||||
<SelectItem value="25">25 items</SelectItem>
|
<SelectItem value="25">{t('Common.ItemsPerPage.25')}</SelectItem>
|
||||||
<div className="p-2 border-t mt-1">
|
<div className="p-2 border-t mt-1">
|
||||||
<Label htmlFor="custom-items" className="text-xs font-medium">Custom (1-100)</Label>
|
<Label htmlFor="custom-items" className="text-xs font-medium">{t('Common.ItemsPerPage.Custom')}</Label>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<Input
|
<Input
|
||||||
id="custom-items"
|
id="custom-items"
|
||||||
@ -239,8 +237,6 @@ export default function Uptime() {
|
|||||||
className="h-8"
|
className="h-8"
|
||||||
defaultValue={itemsPerPage}
|
defaultValue={itemsPerPage}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
// Don't immediately apply the change while typing
|
|
||||||
// Just validate the input for visual feedback
|
|
||||||
const value = parseInt(e.target.value);
|
const value = parseInt(e.target.value);
|
||||||
if (isNaN(value) || value < 1 || value > 100) {
|
if (isNaN(value) || value < 1 || value > 100) {
|
||||||
e.target.classList.add("border-red-500");
|
e.target.classList.add("border-red-500");
|
||||||
@ -249,7 +245,6 @@ export default function Uptime() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
// Apply the change when the input loses focus
|
|
||||||
const value = parseInt(e.target.value);
|
const value = parseInt(e.target.value);
|
||||||
if (value >= 1 && value <= 100) {
|
if (value >= 1 && value <= 100) {
|
||||||
handleItemsPerPageChange(e.target.value);
|
handleItemsPerPageChange(e.target.value);
|
||||||
@ -257,7 +252,6 @@ export default function Uptime() {
|
|||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
// Clear any existing debounce timer to apply immediately
|
|
||||||
if (debounceTimerRef.current) {
|
if (debounceTimerRef.current) {
|
||||||
clearTimeout(debounceTimerRef.current);
|
clearTimeout(debounceTimerRef.current);
|
||||||
debounceTimerRef.current = null;
|
debounceTimerRef.current = null;
|
||||||
@ -265,7 +259,6 @@ export default function Uptime() {
|
|||||||
|
|
||||||
const value = parseInt((e.target as HTMLInputElement).value);
|
const value = parseInt((e.target as HTMLInputElement).value);
|
||||||
if (value >= 1 && value <= 100) {
|
if (value >= 1 && value <= 100) {
|
||||||
// Apply change immediately on Enter
|
|
||||||
const validatedValue = Math.min(Math.max(value, 1), 100);
|
const validatedValue = Math.min(Math.max(value, 1), 100);
|
||||||
setItemsPerPage(validatedValue);
|
setItemsPerPage(validatedValue);
|
||||||
setPagination(prev => ({...prev, currentPage: 1}));
|
setPagination(prev => ({...prev, currentPage: 1}));
|
||||||
@ -275,15 +268,13 @@ export default function Uptime() {
|
|||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
getData(timespan, 1, validatedValue);
|
getData(timespan, 1, validatedValue);
|
||||||
|
|
||||||
// Close the dropdown
|
|
||||||
document.body.click();
|
document.body.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">items</span>
|
<span className="text-xs text-muted-foreground whitespace-nowrap">{t('Common.ItemsPerPage.items')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -297,13 +288,13 @@ export default function Uptime() {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="Select timespan" />
|
<SelectValue placeholder={t('Uptime.TimeRange.Select')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1">Last 1 hour</SelectItem>
|
<SelectItem value="1">{t('Uptime.TimeRange.LastHour')}</SelectItem>
|
||||||
<SelectItem value="2">Last 1 day</SelectItem>
|
<SelectItem value="2">{t('Uptime.TimeRange.LastDay')}</SelectItem>
|
||||||
<SelectItem value="3">Last 7 days</SelectItem>
|
<SelectItem value="3">{t('Uptime.TimeRange.Last7Days')}</SelectItem>
|
||||||
<SelectItem value="4">Last 30 days</SelectItem>
|
<SelectItem value="4">{t('Uptime.TimeRange.Last30Days')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@ -311,7 +302,7 @@ export default function Uptime() {
|
|||||||
|
|
||||||
<div className="pt-4 space-y-4">
|
<div className="pt-4 space-y-4">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-8">Loading...</div>
|
<div className="text-center py-8">{t('Uptime.Messages.Loading')}</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((app) => {
|
data.map((app) => {
|
||||||
const reversedSummary = [...app.uptimeSummary].reverse();
|
const reversedSummary = [...app.uptimeSummary].reverse();
|
||||||
@ -370,10 +361,10 @@ export default function Uptime() {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{entry.missing
|
{entry.missing
|
||||||
? "No data"
|
? t('Uptime.Status.NoData')
|
||||||
: entry.online
|
: entry.online
|
||||||
? "Online"
|
? t('Uptime.Status.Online')
|
||||||
: "Offline"}
|
: t('Uptime.Status.Offline')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip.Arrow className="fill-gray-900" />
|
<Tooltip.Arrow className="fill-gray-900" />
|
||||||
@ -397,8 +388,12 @@ export default function Uptime() {
|
|||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{pagination.totalItems > 0
|
{pagination.totalItems > 0
|
||||||
? `Showing ${((pagination.currentPage - 1) * itemsPerPage) + 1}-${Math.min(pagination.currentPage * itemsPerPage, pagination.totalItems)} of ${pagination.totalItems} items`
|
? t('Uptime.Pagination.Showing', {
|
||||||
: "No items found"}
|
start: ((pagination.currentPage - 1) * itemsPerPage) + 1,
|
||||||
|
end: Math.min(pagination.currentPage * itemsPerPage, pagination.totalItems),
|
||||||
|
total: pagination.totalItems
|
||||||
|
})
|
||||||
|
: t('Uptime.Messages.NoItems')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Pagination>
|
<Pagination>
|
||||||
|
|||||||
403
i18n/languages/de.json
Normal file
403
i18n/languages/de.json
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
{
|
||||||
|
"Common": {
|
||||||
|
"ChangeView": "Ansicht ändern",
|
||||||
|
"ListView": "Listenansicht",
|
||||||
|
"GridView": "Rasteransicht",
|
||||||
|
"optional": "optional",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"since": "seit {date}",
|
||||||
|
"notSet": "Nicht festgelegt",
|
||||||
|
"noData": "Keine Daten",
|
||||||
|
"Loading": "Lade...",
|
||||||
|
"Refresh": "Aktualisieren",
|
||||||
|
"Server": {
|
||||||
|
"CPU": "CPU",
|
||||||
|
"GPU": "GPU",
|
||||||
|
"RAM": "RAM",
|
||||||
|
"Disk": "Festplatte",
|
||||||
|
"OS": "Betriebssystem",
|
||||||
|
"IP": "IP",
|
||||||
|
"Host": "Host",
|
||||||
|
"Temperature": "Temperatur",
|
||||||
|
"Usage": "Auslastung",
|
||||||
|
"Tabs": {
|
||||||
|
"General": "Allgemein",
|
||||||
|
"Hardware": "Hardware",
|
||||||
|
"Host": "Host",
|
||||||
|
"Monitoring": "Überwachung"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ItemsPerPage": {
|
||||||
|
"items": "Einträge",
|
||||||
|
"item": "Eintrag",
|
||||||
|
"4": "4 Einträge",
|
||||||
|
"6": "6 Einträge",
|
||||||
|
"10": "10 Einträge",
|
||||||
|
"15": "15 Einträge",
|
||||||
|
"20": "20 Einträge",
|
||||||
|
"25": "25 Einträge",
|
||||||
|
"Custom": "Benutzerdefiniert (1-100)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Server": {
|
||||||
|
"Hardware": "Hardware",
|
||||||
|
"Network": "Netzwerk",
|
||||||
|
"CurrentUsage": "Aktuelle Auslastung",
|
||||||
|
"UsageHistory": "Nutzungsverlauf",
|
||||||
|
"ResourceUsageHistory": "Ressourcennutzungsverlauf",
|
||||||
|
"TimeRange": {
|
||||||
|
"Select": "Zeitraum auswählen",
|
||||||
|
"LastHour": "Letzte Stunde",
|
||||||
|
"Last24Hours": "Letzte 24 Stunden",
|
||||||
|
"Last7Days": "Letzte 7 Tage",
|
||||||
|
"Last30Days": "Letzte 30 Tage"
|
||||||
|
},
|
||||||
|
"VirtualMachines": "Virtuelle Maschinen",
|
||||||
|
"VirtualMachinesDescription": "Virtuelle Maschinen auf diesem Server",
|
||||||
|
"HardwareInformation": "Hardware-Informationen",
|
||||||
|
"ManagementURL": "Management-URL",
|
||||||
|
"VM": "VM",
|
||||||
|
"Physical": "Physisch",
|
||||||
|
"HostedOn": "Gehostet auf",
|
||||||
|
"NotFound": "Server nicht gefunden",
|
||||||
|
"NotFoundDescription": "Der gesuchte Server konnte nicht gefunden werden."
|
||||||
|
},
|
||||||
|
"Sidebar": {
|
||||||
|
"Main Navigation": "Hauptnavigation",
|
||||||
|
"Dashboard": "Dashboard",
|
||||||
|
"My Infrastructure": "Meine Infrastruktur",
|
||||||
|
"Servers": "Server",
|
||||||
|
"Applications": "Anwendungen",
|
||||||
|
"Uptime": "Uptime",
|
||||||
|
"Network": "Netzwerk",
|
||||||
|
"Settings": "Einstellungen",
|
||||||
|
"Logout": "Abmelden"
|
||||||
|
},
|
||||||
|
"Home": {
|
||||||
|
"TitleUnder": "Dashboard zur Verwaltung Ihrer Serverinfrastruktur",
|
||||||
|
"LoginCardTitle": "Anmeldung",
|
||||||
|
"LoginCardDescription": "Anmeldedaten eingeben",
|
||||||
|
"AuthenticationError": "Authentifizierungsfehler",
|
||||||
|
"Email": "E-Mail",
|
||||||
|
"Password": "Passwort",
|
||||||
|
"SigninButton": "Anmelden",
|
||||||
|
"SigninButtonSigningIn": "Anmeldung läuft..."
|
||||||
|
},
|
||||||
|
"Dashboard": {
|
||||||
|
"Title": "Dashboard",
|
||||||
|
"Servers": {
|
||||||
|
"Title": "Server",
|
||||||
|
"Description": "Übersicht physischer und virtueller Server",
|
||||||
|
"PhysicalServers": "Physische Server",
|
||||||
|
"VirtualServers": "Virtuelle Server",
|
||||||
|
"ManageServers": "Server verwalten"
|
||||||
|
},
|
||||||
|
"Applications": {
|
||||||
|
"Title": "Anwendungen",
|
||||||
|
"Description": "Verwalten Sie Ihre Anwendungen",
|
||||||
|
"OnlineApplications": "Aktive Anwendungen",
|
||||||
|
"ViewAllApplications": "Alle Anwendungen anzeigen"
|
||||||
|
},
|
||||||
|
"Uptime": {
|
||||||
|
"Title": "Uptime",
|
||||||
|
"Description": "Überwachung der Verfügbarkeit",
|
||||||
|
"OnlineApplications": "Online-Anwendungen",
|
||||||
|
"ViewUptimeMetrics": "Uptime-Statistiken anzeigen"
|
||||||
|
},
|
||||||
|
"Network": {
|
||||||
|
"Title": "Netzwerk",
|
||||||
|
"Description": "Netzwerkkonfiguration verwalten",
|
||||||
|
"ActiveConnections": "Aktive Verbindungen",
|
||||||
|
"ViewNetworkDetails": "Netzwerkdetails anzeigen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Servers": {
|
||||||
|
"Title": "Server",
|
||||||
|
"MyInfrastructure": "Meine Infrastruktur",
|
||||||
|
"YourServers": "Ihre Server",
|
||||||
|
"AddServer": {
|
||||||
|
"Title": "Server hinzufügen",
|
||||||
|
"General": {
|
||||||
|
"Title": "Server hinzufügen",
|
||||||
|
"CopyServer": "Server kopieren",
|
||||||
|
"Icon": "Symbol",
|
||||||
|
"IconPlaceholder": "Symbol auswählen",
|
||||||
|
"IconSearchPlaceholder": "Symbole suchen...",
|
||||||
|
"Preview": "Vorschau",
|
||||||
|
"Name": "Name",
|
||||||
|
"OperatingSystem": "Betriebssystem",
|
||||||
|
"OperatingSystemPlaceholder": "OS auswählen",
|
||||||
|
"IPAdress": "IP-Adresse",
|
||||||
|
"ManagementURL": "Management-URL",
|
||||||
|
"ManagementURLTooltip": "Link zur Verwaltungsoberfläche (z.B. Proxmox oder Portainer)"
|
||||||
|
},
|
||||||
|
"Host": {
|
||||||
|
"MarkAsHostServer": "Als Host-Server markieren",
|
||||||
|
"SelectHostServer": "Host-Server auswählen",
|
||||||
|
"SelectHostServerPlaceholder": "Host-Server auswählen",
|
||||||
|
"NoHostServer": "Kein Host-Server"
|
||||||
|
},
|
||||||
|
"Monitoring": {
|
||||||
|
"Enable": "Überwachung aktivieren",
|
||||||
|
"URL": "Monitoring-URL",
|
||||||
|
"SetupTitle": "Erforderliche Serverkonfiguration",
|
||||||
|
"SetupDescription": "Zur Aktivierung benötigen Sie Glances auf dem Server. Beispiel-Docker-Compose-Konfiguration:"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ServerCard": {
|
||||||
|
"HardwareInformation": "Hardware-Informationen",
|
||||||
|
"ViewDetails": "Details anzeigen",
|
||||||
|
"OpenManagementURL": "Management-URL öffnen",
|
||||||
|
"EditServer": "Server bearbeiten",
|
||||||
|
"DeleteServer": "Server löschen",
|
||||||
|
"HostedVMs": "Gehostete VMs",
|
||||||
|
"ResourceUsage": "Ressourcennutzung",
|
||||||
|
"DeleteConfirmation": {
|
||||||
|
"Title": "{name} löschen",
|
||||||
|
"Description": "Sind Sie sicher? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Delete": "Löschen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EditServer": {
|
||||||
|
"Title": "{name} bearbeiten",
|
||||||
|
"Save": "Speichern",
|
||||||
|
"General": {
|
||||||
|
"Title": "Allgemein",
|
||||||
|
"Icon": "Symbol",
|
||||||
|
"Name": "Name",
|
||||||
|
"OperatingSystem": "Betriebssystem",
|
||||||
|
"IPAddress": "IP-Adresse",
|
||||||
|
"ManagementURL": "Management-URL"
|
||||||
|
},
|
||||||
|
"Hardware": {
|
||||||
|
"Title": "Hardware",
|
||||||
|
"CPU": "CPU",
|
||||||
|
"GPU": "GPU",
|
||||||
|
"RAM": "RAM",
|
||||||
|
"Disk": "Festplatte"
|
||||||
|
},
|
||||||
|
"Host": {
|
||||||
|
"Title": "Host",
|
||||||
|
"MarkAsHostServer": "Als Host-Server markieren",
|
||||||
|
"CannotDisableHost": "Kann nicht deaktiviert werden, solange VMs gehostet werden",
|
||||||
|
"SelectHostServer": "Host-Server auswählen"
|
||||||
|
},
|
||||||
|
"Monitoring": {
|
||||||
|
"Title": "Überwachung",
|
||||||
|
"Enable": "Überwachung aktivieren",
|
||||||
|
"URL": "Monitoring-URL",
|
||||||
|
"SetupTitle": "Erforderliche Serverkonfiguration",
|
||||||
|
"SetupDescription": "Beispiel-Docker-Compose-Konfiguration für Glances:"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Pagination": {
|
||||||
|
"Showing": "Zeige {start}-{end} von {total} Servern",
|
||||||
|
"NoServers": "Keine Server gefunden"
|
||||||
|
},
|
||||||
|
"Search": {
|
||||||
|
"Placeholder": "Suche..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Applications": {
|
||||||
|
"Title": "Ihre Anwendungen",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"MyInfrastructure": "Meine Infrastruktur",
|
||||||
|
"Applications": "Anwendungen"
|
||||||
|
},
|
||||||
|
"Views": {
|
||||||
|
"ChangeView": "Ansicht ändern",
|
||||||
|
"ListView": "Listenansicht",
|
||||||
|
"GridView": "Rasteransicht",
|
||||||
|
"CompactView": "Kompaktansicht"
|
||||||
|
},
|
||||||
|
"Add": {
|
||||||
|
"Title": "Anwendung hinzufügen",
|
||||||
|
"Name": "Name",
|
||||||
|
"NamePlaceholder": "z.B. Portainer",
|
||||||
|
"Server": "Server",
|
||||||
|
"SelectServer": "Server auswählen",
|
||||||
|
"Description": "Beschreibung",
|
||||||
|
"DescriptionPlaceholder": "Anwendungsbeschreibung",
|
||||||
|
"IconURL": "Symbol-URL",
|
||||||
|
"IconURLPlaceholder": "https://example.com/icon.png",
|
||||||
|
"PublicURL": "Öffentliche URL",
|
||||||
|
"PublicURLPlaceholder": "https://example.com",
|
||||||
|
"LocalURL": "Lokale URL",
|
||||||
|
"LocalURLPlaceholder": "http://localhost:3000",
|
||||||
|
"CustomUptimeCheck": "Benutzerdefinierte Uptime-URL",
|
||||||
|
"CustomUptimeCheckTooltip": "Ersetzt die öffentliche URL für Uptime-Checks",
|
||||||
|
"UptimeCheckURL": "Uptime-Prüf-URL",
|
||||||
|
"UptimeCheckURLPlaceholder": "https://example.com/status"
|
||||||
|
},
|
||||||
|
"Edit": {
|
||||||
|
"Title": "Anwendung bearbeiten",
|
||||||
|
"SaveButton": "Änderungen speichern"
|
||||||
|
},
|
||||||
|
"Card": {
|
||||||
|
"NoServer": "Kein Server",
|
||||||
|
"PublicURL": "Öffentliche URL",
|
||||||
|
"LocalURL": "Lokale URL",
|
||||||
|
"Icon": "Symbol",
|
||||||
|
"Image": "Bild"
|
||||||
|
},
|
||||||
|
"Messages": {
|
||||||
|
"AddServerFirst": "Bitte zuerst Server hinzufügen",
|
||||||
|
"AddSuccess": "Anwendung erfolgreich hinzugefügt",
|
||||||
|
"EditSuccess": "Änderungen gespeichert",
|
||||||
|
"DeleteSuccess": "Anwendung gelöscht",
|
||||||
|
"GetError": "Fehler beim Laden",
|
||||||
|
"EditError": "Speichern fehlgeschlagen",
|
||||||
|
"DeleteError": "Löschen fehlgeschlagen",
|
||||||
|
"NumberValidation": "Bitte Zahl zwischen 1-100 eingeben"
|
||||||
|
},
|
||||||
|
"Pagination": {
|
||||||
|
"Showing": "Zeige {start}-{end} von {total} Anwendungen",
|
||||||
|
"NoApplications": "Keine Anwendungen gefunden"
|
||||||
|
},
|
||||||
|
"Search": {
|
||||||
|
"Placeholder": "Suche..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Uptime": {
|
||||||
|
"Title": "Uptime",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"MyInfrastructure": "Meine Infrastruktur",
|
||||||
|
"Uptime": "Uptime"
|
||||||
|
},
|
||||||
|
"TimeRange": {
|
||||||
|
"Select": "Zeitraum wählen",
|
||||||
|
"LastHour": "Letzte Stunde",
|
||||||
|
"LastDay": "Letzter Tag",
|
||||||
|
"Last7Days": "Letzte 7 Tage",
|
||||||
|
"Last30Days": "Letzte 30 Tage"
|
||||||
|
},
|
||||||
|
"Status": {
|
||||||
|
"Online": "Online",
|
||||||
|
"Offline": "Offline",
|
||||||
|
"NoData": "Keine Daten"
|
||||||
|
},
|
||||||
|
"Messages": {
|
||||||
|
"NumberValidation": "Bitte Zahl zwischen 1-100",
|
||||||
|
"NoItems": "Keine Einträge",
|
||||||
|
"Loading": "Lade..."
|
||||||
|
},
|
||||||
|
"Pagination": {
|
||||||
|
"Showing": "Zeige {start}-{end} von {total} Einträgen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Network": {
|
||||||
|
"Title": "Netzwerk",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"MyInfrastructure": "Meine Infrastruktur",
|
||||||
|
"Network": "Netzwerk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Settings": {
|
||||||
|
"Title": "Einstellungen",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"Dashboard": "Dashboard",
|
||||||
|
"Settings": "Einstellungen"
|
||||||
|
},
|
||||||
|
"UserSettings": {
|
||||||
|
"Title": "Benutzereinstellungen",
|
||||||
|
"Description": "E-Mail, Passwort und Kontoeinstellungen verwalten",
|
||||||
|
"ChangeEmail": {
|
||||||
|
"Title": "E-Mail ändern",
|
||||||
|
"Placeholder": "Neue E-Mail",
|
||||||
|
"Button": "E-Mail ändern",
|
||||||
|
"Success": "E-Mail geändert",
|
||||||
|
"Error": "Fehler"
|
||||||
|
},
|
||||||
|
"ChangePassword": {
|
||||||
|
"Title": "Passwort ändern",
|
||||||
|
"OldPassword": "Altes Passwort",
|
||||||
|
"NewPassword": "Neues Passwort",
|
||||||
|
"ConfirmPassword": "Passwort bestätigen",
|
||||||
|
"Button": "Passwort ändern",
|
||||||
|
"Success": "Passwort geändert",
|
||||||
|
"Error": "Fehler",
|
||||||
|
"PasswordsDontMatch": "Passwörter stimmen nicht überein",
|
||||||
|
"AllFieldsRequired": "Alle Felder benötigt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ThemeSettings": {
|
||||||
|
"Title": "Design-Einstellungen",
|
||||||
|
"Description": "Wählen Sie zwischen Hell, Dunkel oder System",
|
||||||
|
"Light": "Hell",
|
||||||
|
"Dark": "Dunkel",
|
||||||
|
"System": "System"
|
||||||
|
},
|
||||||
|
"Notifications": {
|
||||||
|
"Title": "Benachrichtigungen",
|
||||||
|
"Description": "Erhalten Sie Warnungen bei Statusänderungen",
|
||||||
|
"AddChannel": "Kanal hinzufügen",
|
||||||
|
"NoNotifications": "Keine Benachrichtigungen",
|
||||||
|
"NoNotificationsDescription": "Fügen Sie einen Benachrichtigungskanal hinzu",
|
||||||
|
"AddNotification": {
|
||||||
|
"Title": "Benachrichtigung hinzufügen",
|
||||||
|
"Name": "Name (optional)",
|
||||||
|
"Type": "Typ",
|
||||||
|
"SMTP": {
|
||||||
|
"Title": "SMTP",
|
||||||
|
"Host": "SMTP-Host",
|
||||||
|
"Port": "SMTP-Port",
|
||||||
|
"Secure": "Sichere Verbindung (TLS/SSL)",
|
||||||
|
"Username": "SMTP-Benutzername",
|
||||||
|
"Password": "SMTP-Passwort",
|
||||||
|
"From": "Absenderadresse",
|
||||||
|
"To": "Empfängeradresse"
|
||||||
|
},
|
||||||
|
"Telegram": {
|
||||||
|
"Title": "Telegram",
|
||||||
|
"Token": "Bot-Token",
|
||||||
|
"ChatId": "Chat-ID"
|
||||||
|
},
|
||||||
|
"Discord": {
|
||||||
|
"Title": "Discord",
|
||||||
|
"Webhook": "Webhook-URL"
|
||||||
|
},
|
||||||
|
"Gotify": {
|
||||||
|
"Title": "Gotify",
|
||||||
|
"Url": "Gotify-URL",
|
||||||
|
"Token": "Gotify-Token"
|
||||||
|
},
|
||||||
|
"Ntfy": {
|
||||||
|
"Title": "Ntfy",
|
||||||
|
"Url": "Ntfy-URL",
|
||||||
|
"Token": "Ntfy-Token"
|
||||||
|
},
|
||||||
|
"Pushover": {
|
||||||
|
"Title": "Pushover",
|
||||||
|
"Url": "Pushover-URL",
|
||||||
|
"Token": "Pushover-Token",
|
||||||
|
"User": "Pushover-Benutzer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CustomizeText": {
|
||||||
|
"Display": "Benachrichtigungstext anpassen",
|
||||||
|
"Title": "Textanpassung",
|
||||||
|
"Application": "Text für Anwendungen",
|
||||||
|
"Server": "Text für Server",
|
||||||
|
"Placeholders": {
|
||||||
|
"Title": "Verfügbare Platzhalter:",
|
||||||
|
"Server": {
|
||||||
|
"Title": "Server:",
|
||||||
|
"Name": "!name - Servername",
|
||||||
|
"Status": "!status - Status (online/offline)"
|
||||||
|
},
|
||||||
|
"Application": {
|
||||||
|
"Title": "Anwendung:",
|
||||||
|
"Name": "!name - Anwendungsname",
|
||||||
|
"Url": "!url - Anwendungs-URL",
|
||||||
|
"Status": "!status - Status (online/offline)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ActiveChannels": "Aktive Kanäle",
|
||||||
|
"Test": "Testen",
|
||||||
|
"Delete": "Löschen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -199,6 +199,207 @@
|
|||||||
"Search": {
|
"Search": {
|
||||||
"Placeholder": "Type to search..."
|
"Placeholder": "Type to search..."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Applications": {
|
||||||
|
"Title": "Your Applications",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"MyInfrastructure": "My Infrastructure",
|
||||||
|
"Applications": "Applications"
|
||||||
|
},
|
||||||
|
"Views": {
|
||||||
|
"ChangeView": "Change view",
|
||||||
|
"ListView": "List View",
|
||||||
|
"GridView": "Grid View",
|
||||||
|
"CompactView": "Compact View"
|
||||||
|
},
|
||||||
|
"Add": {
|
||||||
|
"Title": "Add an application",
|
||||||
|
"Name": "Name",
|
||||||
|
"NamePlaceholder": "e.g. Portainer",
|
||||||
|
"Server": "Server",
|
||||||
|
"SelectServer": "Select server",
|
||||||
|
"Description": "Description",
|
||||||
|
"DescriptionPlaceholder": "Application description",
|
||||||
|
"IconURL": "Icon URL",
|
||||||
|
"IconURLPlaceholder": "https://example.com/icon.png",
|
||||||
|
"PublicURL": "Public URL",
|
||||||
|
"PublicURLPlaceholder": "https://example.com",
|
||||||
|
"LocalURL": "Local URL",
|
||||||
|
"LocalURLPlaceholder": "http://localhost:3000",
|
||||||
|
"CustomUptimeCheck": "Custom Uptime Check URL",
|
||||||
|
"CustomUptimeCheckTooltip": "When enabled, this URL replaces the Public URL for uptime monitoring checks",
|
||||||
|
"UptimeCheckURL": "Uptime Check URL",
|
||||||
|
"UptimeCheckURLPlaceholder": "https://example.com/status"
|
||||||
|
},
|
||||||
|
"Edit": {
|
||||||
|
"Title": "Edit Application",
|
||||||
|
"SaveButton": "Save Changes"
|
||||||
|
},
|
||||||
|
"Card": {
|
||||||
|
"Server": "Server",
|
||||||
|
"NoServer": "No server",
|
||||||
|
"PublicURL": "Public URL",
|
||||||
|
"LocalURL": "Local URL",
|
||||||
|
"Icon": "Icon",
|
||||||
|
"Image": "Image"
|
||||||
|
},
|
||||||
|
"Messages": {
|
||||||
|
"AddServerFirst": "You must first add a server.",
|
||||||
|
"AddSuccess": "Application added successfully",
|
||||||
|
"EditSuccess": "Application edited successfully",
|
||||||
|
"DeleteSuccess": "Application deleted successfully",
|
||||||
|
"GetError": "Failed to get applications",
|
||||||
|
"EditError": "Failed to edit application",
|
||||||
|
"DeleteError": "Failed to delete application",
|
||||||
|
"NumberValidation": "Please enter a number between 1 and 100"
|
||||||
|
},
|
||||||
|
"Pagination": {
|
||||||
|
"Showing": "Showing {start}-{end} of {total} applications",
|
||||||
|
"NoApplications": "No applications found"
|
||||||
|
},
|
||||||
|
"Search": {
|
||||||
|
"Placeholder": "Type to search..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Uptime": {
|
||||||
|
"Title": "Uptime",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"MyInfrastructure": "My Infrastructure",
|
||||||
|
"Uptime": "Uptime"
|
||||||
|
},
|
||||||
|
"TimeRange": {
|
||||||
|
"Select": "Select timespan",
|
||||||
|
"LastHour": "Last 1 hour",
|
||||||
|
"LastDay": "Last 1 day",
|
||||||
|
"Last7Days": "Last 7 days",
|
||||||
|
"Last30Days": "Last 30 days"
|
||||||
|
},
|
||||||
|
"Status": {
|
||||||
|
"Online": "Online",
|
||||||
|
"Offline": "Offline",
|
||||||
|
"NoData": "No data"
|
||||||
|
},
|
||||||
|
"Messages": {
|
||||||
|
"NumberValidation": "Please enter a number between 1 and 100",
|
||||||
|
"NoItems": "No items found",
|
||||||
|
"Loading": "Loading..."
|
||||||
|
},
|
||||||
|
"Pagination": {
|
||||||
|
"Showing": "Showing {start}-{end} of {total} items"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Network": {
|
||||||
|
"Title": "Network",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"MyInfrastructure": "My Infrastructure",
|
||||||
|
"Network": "Network"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Settings": {
|
||||||
|
"Title": "Settings",
|
||||||
|
"Breadcrumb": {
|
||||||
|
"Dashboard": "Dashboard",
|
||||||
|
"Settings": "Settings"
|
||||||
|
},
|
||||||
|
"UserSettings": {
|
||||||
|
"Title": "User Settings",
|
||||||
|
"Description": "Manage your user settings here. You can change your email, password, and other account settings.",
|
||||||
|
"ChangeEmail": {
|
||||||
|
"Title": "Change Email",
|
||||||
|
"Placeholder": "Enter new email",
|
||||||
|
"Button": "Change Email",
|
||||||
|
"Success": "Email changed successfully.",
|
||||||
|
"Error": "Error"
|
||||||
|
},
|
||||||
|
"ChangePassword": {
|
||||||
|
"Title": "Change Password",
|
||||||
|
"OldPassword": "Enter old password",
|
||||||
|
"NewPassword": "Enter new password",
|
||||||
|
"ConfirmPassword": "Confirm new password",
|
||||||
|
"Button": "Change Password",
|
||||||
|
"Success": "Password changed successfully.",
|
||||||
|
"Error": "Error",
|
||||||
|
"PasswordsDontMatch": "Passwords do not match",
|
||||||
|
"AllFieldsRequired": "All fields are required"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ThemeSettings": {
|
||||||
|
"Title": "Theme Settings",
|
||||||
|
"Description": "Select a theme for the application. You can choose between light, dark, or system theme.",
|
||||||
|
"Light": "Light",
|
||||||
|
"Dark": "Dark",
|
||||||
|
"System": "System"
|
||||||
|
},
|
||||||
|
"Notifications": {
|
||||||
|
"Title": "Notifications",
|
||||||
|
"Description": "Set up notifications to get instantly alerted when an application changes status.",
|
||||||
|
"AddChannel": "Add Notification Channel",
|
||||||
|
"NoNotifications": "No notifications configured",
|
||||||
|
"NoNotificationsDescription": "Add a notification channel to get alerted when your applications change status.",
|
||||||
|
"AddNotification": {
|
||||||
|
"Title": "Add Notification",
|
||||||
|
"Name": "Notification Name (optional)",
|
||||||
|
"Type": "Notification Type",
|
||||||
|
"SMTP": {
|
||||||
|
"Title": "SMTP",
|
||||||
|
"Host": "SMTP Host",
|
||||||
|
"Port": "SMTP Port",
|
||||||
|
"Secure": "Secure Connection (TLS/SSL)",
|
||||||
|
"Username": "SMTP Username",
|
||||||
|
"Password": "SMTP Password",
|
||||||
|
"From": "From Address",
|
||||||
|
"To": "To Address"
|
||||||
|
},
|
||||||
|
"Telegram": {
|
||||||
|
"Title": "Telegram",
|
||||||
|
"Token": "Bot Token",
|
||||||
|
"ChatId": "Chat ID"
|
||||||
|
},
|
||||||
|
"Discord": {
|
||||||
|
"Title": "Discord",
|
||||||
|
"Webhook": "Webhook URL"
|
||||||
|
},
|
||||||
|
"Gotify": {
|
||||||
|
"Title": "Gotify",
|
||||||
|
"Url": "Gotify URL",
|
||||||
|
"Token": "Gotify Token"
|
||||||
|
},
|
||||||
|
"Ntfy": {
|
||||||
|
"Title": "Ntfy",
|
||||||
|
"Url": "Ntfy URL",
|
||||||
|
"Token": "Ntfy Token"
|
||||||
|
},
|
||||||
|
"Pushover": {
|
||||||
|
"Title": "Pushover",
|
||||||
|
"Url": "Pushover URL",
|
||||||
|
"Token": "Pushover Token",
|
||||||
|
"User": "Pushover User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CustomizeText": {
|
||||||
|
"Display": "Customize Notification Text",
|
||||||
|
"Title": "Customize Notification Text",
|
||||||
|
"Application": "Notification Text for Applications",
|
||||||
|
"Server": "Notification Text for Servers",
|
||||||
|
"Placeholders": {
|
||||||
|
"Title": "You can use the following placeholders in the text:",
|
||||||
|
"Server": {
|
||||||
|
"Title": "Server related:",
|
||||||
|
"Name": "!name - The name of the server",
|
||||||
|
"Status": "!status - The current status of the server (online/offline)"
|
||||||
|
},
|
||||||
|
"Application": {
|
||||||
|
"Title": "Application related:",
|
||||||
|
"Name": "!name - The name of the application",
|
||||||
|
"Url": "!url - The URL where the application is hosted",
|
||||||
|
"Status": "!status - The current status of the application (online/offline)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ActiveChannels": "Active Notification Channels",
|
||||||
|
"Test": "Test",
|
||||||
|
"Delete": "Delete"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user