mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Feat/monitoring (#1267) Cloud Version
* feat: add start monitoring remote servers * reafctor: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: * refactor: add metrics * feat: add disk monitoring * refactor: translate to english * refacotor: add stats * refactor: remove color * feat: add log server metrics * refactor: remove unused deps * refactor: add origin * refactor: add logs * refactor: update * feat: add series monitoring * refactor: add system monitoring * feat: add benchmark to optimize data * refactor: update fn * refactor: remove comments * refactor: update * refactor: exclude items * feat: add refresh rate * feat: add monitoring remote servers * refactor: update * refactor: remove unsued volumes * refactor: update monitoring * refactor: add more presets * feat: add container metrics * feat: add docker monitoring * refactor: update conversion * refactor: remove unused code * refactor: update * refactor: add docker compose logs * refactor: add docker cli * refactor: add install curl * refactor: add get update * refactor: add monitoring remote servers * refactor: add containers config * feat: add container specification * refactor: update path * refactor: add server filter * refactor: simplify logic * fix: verify if file exist before get stats * refactor: update * refactor: remove unused deps * test: add test for containers * refactor: update * refactor add memory collector * refactor: update * refactor: update * refactor: update * refactor: remove * refactor: add memory * refactor: add server memory usage * refactor: change memory * refactor: update * refactor: update * refactor: add container metrics * refactor: comment code * refactor: mount proc bind * refactor: change interval with node cron * refactor: remove opening file * refactor: use streams * refactor: remove unused ws * refactor: disable live when is all * refactor: add sqlite * refactor: update * feat: add golang benchmark * refactor: update go * refactor: update dockerfile * refactor: update db * refactor: add env * refactor: separate logic * refactor: split logic * refactor: update logs * refactor: update dockerfile * refactor: hide .env * refactor: update * chore: hide ,.ebnv * refactor: add end angle * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update * refactor: update monitoring * refactor: add mount db * refactor: add metrics and url callback * refactor: add middleware * refactor: add threshold property * feat: add memory and cpu threshold notification * feat: send notifications to the server * feat: add metrics for dokploy server * refactor: add dokploy server to monitoring * refactor: update methods * refactor: add admin to useeffect * refactor: stop monitoring containers if elements are 0 * refactor: cancel request if appName is empty * refactor: reuse methods * chore; add feat monitoring * refactor: set base url * refactor: adjust monitoring * refactor: delete migrations * feat: add columns * fix: add missing flag * refactor: add free metrics * refactor: add paid monitoring * refactor: update methods * feat: improve ui * feat: add container stats * refactor: add all container metrics * refactor: add color primary * refactor: change default rate limiting refresher * refactor: update retention days * refactor: use json instead of individual properties * refactor: lint * refactor: pass json env * refactor: update * refactor: delete * refactor: update * refactor: fix types * refactor: add retention days * chore: add license * refactor: create db * refactor: update path * refactor: update setup * refactor: update * refactor: create files * refactor: update * refactor: delete * refactor: update * refactor: update token metrics * fix: typechecks * refactor: setup web server * refactor: update error handling and add monitoring * refactor: add local storage save * refactor: add spacing * refactor: update * refactor: upgrade drizzle * refactor: delete * refactor: uppgrade drizzle kit * refactor: update search with jsonB * chore: upgrade drizzle * chore: update packages * refactor: add missing type * refactor: add serverType * refactor: update url * refactor: update * refactor: update * refactor: hide monitoring on self hosted * refactor: update server * refactor: update * refactor: update * refactor: pin node version
This commit is contained in:
@@ -49,6 +49,7 @@ const notificationBaseSchema = z.object({
|
||||
databaseBackup: z.boolean().default(false),
|
||||
dokployRestart: z.boolean().default(false),
|
||||
dockerCleanup: z.boolean().default(false),
|
||||
serverThreshold: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const notificationSchema = z.discriminatedUnion("type", [
|
||||
@@ -204,6 +205,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
channel: notification.slack?.channel || "",
|
||||
name: notification.name,
|
||||
type: notification.notificationType,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "telegram") {
|
||||
form.reset({
|
||||
@@ -216,6 +218,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
type: notification.notificationType,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "discord") {
|
||||
form.reset({
|
||||
@@ -228,6 +231,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
decoration: notification.discord?.decoration || undefined,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "email") {
|
||||
form.reset({
|
||||
@@ -244,6 +248,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
fromAddress: notification.email?.fromAddress,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "gotify") {
|
||||
form.reset({
|
||||
@@ -280,6 +285,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dokployRestart,
|
||||
databaseBackup,
|
||||
dockerCleanup,
|
||||
serverThreshold,
|
||||
} = data;
|
||||
let promise: Promise<unknown> | null = null;
|
||||
if (data.type === "slack") {
|
||||
@@ -294,6 +300,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dockerCleanup: dockerCleanup,
|
||||
slackId: notification?.slackId || "",
|
||||
notificationId: notificationId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "telegram") {
|
||||
promise = telegramMutation.mutateAsync({
|
||||
@@ -307,6 +314,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
telegramId: notification?.telegramId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "discord") {
|
||||
promise = discordMutation.mutateAsync({
|
||||
@@ -320,6 +328,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
discordId: notification?.discordId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "email") {
|
||||
promise = emailMutation.mutateAsync({
|
||||
@@ -337,6 +346,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
emailId: notification?.emailId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "gotify") {
|
||||
promise = gotifyMutation.mutateAsync({
|
||||
@@ -955,6 +965,30 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverThreshold"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Server Threshold</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when the server threshold is
|
||||
reached.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Eye, EyeOff, LayoutDashboardIcon, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
const Schema = z.object({
|
||||
metricsConfig: z.object({
|
||||
server: z.object({
|
||||
refreshRate: z.number().min(2, {
|
||||
message: "Server Refresh Rate is required",
|
||||
}),
|
||||
port: z.number().min(1, {
|
||||
message: "Port is required",
|
||||
}),
|
||||
token: z.string(),
|
||||
urlCallback: z.string(),
|
||||
retentionDays: z.number().min(1, {
|
||||
message: "Retention days must be at least 1",
|
||||
}),
|
||||
thresholds: z.object({
|
||||
cpu: z.number().min(0),
|
||||
memory: z.number().min(0),
|
||||
}),
|
||||
cronJob: z.string().min(1, {
|
||||
message: "Cron Job is required",
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number().min(2, {
|
||||
message: "Container Refresh Rate is required",
|
||||
}),
|
||||
services: z.object({
|
||||
include: z.array(z.string()).optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
const { data, isLoading } = serverId
|
||||
? api.server.one.useQuery(
|
||||
{
|
||||
serverId: serverId || "",
|
||||
},
|
||||
{
|
||||
enabled: !!serverId,
|
||||
},
|
||||
)
|
||||
: api.admin.one.useQuery();
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
|
||||
const extractServicesFromProjects = (projects: any[] | undefined) => {
|
||||
if (!projects) return [];
|
||||
|
||||
const allServices = projects.flatMap((project) => {
|
||||
const services = extractServices(project);
|
||||
return serverId
|
||||
? services
|
||||
.filter((service) => service.serverId === serverId)
|
||||
.map((service) => service.appName)
|
||||
: services.map((service) => service.appName);
|
||||
});
|
||||
|
||||
return [...new Set(allServices)];
|
||||
};
|
||||
|
||||
const services = extractServicesFromProjects(projects);
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(Schema),
|
||||
defaultValues: {
|
||||
metricsConfig: {
|
||||
server: {
|
||||
refreshRate: 20,
|
||||
port: 4500,
|
||||
token: "",
|
||||
urlCallback: `${url}/api/trpc/notification.receiveNotification`,
|
||||
retentionDays: 7,
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
cronJob: "",
|
||||
},
|
||||
containers: {
|
||||
refreshRate: 20,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
metricsConfig: {
|
||||
server: {
|
||||
refreshRate: data?.metricsConfig?.server?.refreshRate,
|
||||
port: data?.metricsConfig?.server?.port,
|
||||
token: data?.metricsConfig?.server?.token || generateToken(),
|
||||
urlCallback:
|
||||
data?.metricsConfig?.server?.urlCallback ||
|
||||
`${url}/api/trpc/notification.receiveNotification`,
|
||||
retentionDays: data?.metricsConfig?.server?.retentionDays || 5,
|
||||
thresholds: {
|
||||
cpu: data?.metricsConfig?.server?.thresholds?.cpu,
|
||||
memory: data?.metricsConfig?.server?.thresholds?.memory,
|
||||
},
|
||||
cronJob: data?.metricsConfig?.server?.cronJob || "0 0 * * *",
|
||||
},
|
||||
containers: {
|
||||
refreshRate: data?.metricsConfig?.containers?.refreshRate,
|
||||
services: {
|
||||
include: data?.metricsConfig?.containers?.services?.include,
|
||||
exclude: data?.metricsConfig?.containers?.services?.exclude,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [data, url]);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchExclude, setSearchExclude] = useState("");
|
||||
const [showToken, setShowToken] = useState(false);
|
||||
|
||||
const availableServices = services?.filter(
|
||||
(service) =>
|
||||
!form
|
||||
.watch("metricsConfig.containers.services.include")
|
||||
?.some((s) => s === service) &&
|
||||
!form
|
||||
.watch("metricsConfig.containers.services.exclude")
|
||||
?.includes(service) &&
|
||||
service.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
const availableServicesToExclude = [
|
||||
...(services?.filter(
|
||||
(service) =>
|
||||
!form
|
||||
.watch("metricsConfig.containers.services.exclude")
|
||||
?.includes(service) &&
|
||||
!form
|
||||
.watch("metricsConfig.containers.services.include")
|
||||
?.some((s) => s === service) &&
|
||||
service.toLowerCase().includes(searchExclude.toLowerCase()),
|
||||
) ?? []),
|
||||
...(!form.watch("metricsConfig.containers.services.exclude")?.includes("*")
|
||||
? ["*"]
|
||||
: []),
|
||||
];
|
||||
|
||||
const { mutateAsync } = serverId
|
||||
? api.server.setupMonitoring.useMutation()
|
||||
: api.admin.setupMonitoring.useMutation();
|
||||
|
||||
const generateToken = () => {
|
||||
const array = new Uint8Array(64);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
"",
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = async (values: Schema) => {
|
||||
await mutateAsync({
|
||||
serverId: serverId || "",
|
||||
metricsConfig: values.metricsConfig,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Server updated successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the server");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
|
||||
Monitoring
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Monitor your servers and containers in realtime with notifications
|
||||
when they reach their thresholds.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 py-6 border-t">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-4"
|
||||
>
|
||||
<AlertBlock>
|
||||
Using a lower refresh rate will make your CPU and memory usage
|
||||
higher, we recommend 30-60 seconds
|
||||
</AlertBlock>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.refreshRate"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
||||
<FormLabel>Server Refresh Rate</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder="10" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please set the refresh rate for the server in seconds
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.containers.refreshRate"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
||||
<FormLabel>Container Refresh Rate</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder="10" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please set the refresh rate for the containers in seconds
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.cronJob"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cron Job</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="0 0 * * *" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Cron job for cleaning up metrics
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.retentionDays"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server Retention Days</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Number of days to retain server metrics data
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.port"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder="4500" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Please set the port for the metrics server
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.containers.services.include"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Include Services</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Add Service</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[300px] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search service..."
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
{availableServices?.length === 0 ? (
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
No services available.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CommandEmpty>
|
||||
No service found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableServices?.map((service) => (
|
||||
<CommandItem
|
||||
key={service}
|
||||
value={service}
|
||||
onSelect={() => {
|
||||
field.onChange([
|
||||
...(field.value ?? []),
|
||||
service,
|
||||
]);
|
||||
setSearch("");
|
||||
}}
|
||||
>
|
||||
{service}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{field.value?.map((service) => (
|
||||
<Badge
|
||||
key={service}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{service}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 p-0"
|
||||
onClick={() => {
|
||||
field.onChange(
|
||||
field.value?.filter((s) => s !== service),
|
||||
);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
<FormDescription>
|
||||
Services to monitor.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.containers.services.exclude"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Exclude Services</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Add Service</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[300px] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search service..."
|
||||
value={searchExclude}
|
||||
onValueChange={setSearchExclude}
|
||||
/>
|
||||
{availableServicesToExclude?.length === 0 ? (
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
No services available.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CommandEmpty>
|
||||
No service found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableServicesToExclude.map(
|
||||
(service) => (
|
||||
<CommandItem
|
||||
key={service}
|
||||
value={service}
|
||||
onSelect={() => {
|
||||
field.onChange([
|
||||
...(field.value ?? []),
|
||||
service,
|
||||
]);
|
||||
setSearchExclude("");
|
||||
}}
|
||||
>
|
||||
{service}
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{field.value?.map((service, index) => (
|
||||
<Badge
|
||||
key={service}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{service}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 p-0"
|
||||
onClick={() => {
|
||||
field.onChange(
|
||||
field.value?.filter((_, i) => i !== index),
|
||||
);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
<FormDescription>
|
||||
Services to exclude from monitoring
|
||||
</FormDescription>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.thresholds.cpu"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>CPU Threshold (%)</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Alert when CPU usage exceeds this percentage
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.thresholds.memory"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Memory Threshold (%)</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Alert when memory usage exceeds this percentage
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Metrics Token</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type={showToken ? "text" : "password"}
|
||||
placeholder="Enter your metrics token"
|
||||
{...field}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
title={showToken ? "Hide token" : "Show token"}
|
||||
>
|
||||
{showToken ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const newToken = generateToken();
|
||||
form.setValue(
|
||||
"metricsConfig.server.token",
|
||||
newToken,
|
||||
);
|
||||
toast.success("Token generated successfully");
|
||||
}}
|
||||
title="Generate new token"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Token for authenticating metrics requests
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metricsConfig.server.urlCallback"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Metrics Callback URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://your-callback-url.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
URL where metrics will be sent
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button type="submit" isLoading={form.formState.isSubmitting}>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon, ExternalLinkIcon, ServerIcon } from "lucide-react";
|
||||
@@ -30,6 +31,7 @@ import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
import { EditScript } from "./edit-script";
|
||||
import { GPUSupport } from "./gpu-support";
|
||||
import { SecurityAudit } from "./security-audit";
|
||||
import { SetupMonitoring } from "./setup-monitoring";
|
||||
import { ValidateServer } from "./validate-server";
|
||||
|
||||
interface Props {
|
||||
@@ -48,7 +50,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
);
|
||||
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
@@ -112,11 +114,19 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
</AlertBlock>
|
||||
|
||||
<Tabs defaultValue="ssh-keys">
|
||||
<TabsList className="grid grid-cols-5 w-[700px]">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"grid w-[700px]",
|
||||
isCloud ? "grid-cols-6" : "grid-cols-5",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="validate">Validate</TabsTrigger>
|
||||
<TabsTrigger value="audit">Security</TabsTrigger>
|
||||
{isCloud && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
@@ -309,6 +319,16 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
<SecurityAudit serverId={serverId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="monitoring"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm pt-3">
|
||||
<div className="rounded-xl bg-background shadow-md border">
|
||||
<SetupMonitoring serverId={serverId} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gpu-setup"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { useState } from "react";
|
||||
import { ShowPaidMonitoring } from "../../monitoring/paid/servers/show-paid-monitoring";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const ShowMonitoringModal = ({ url, token }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Show Monitoring
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
||||
<div className="flex gap-4 py-4 w-full">
|
||||
<ShowPaidMonitoring BASE_URL={url} token={token} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -38,6 +38,7 @@ import { ShowServerActions } from "./actions/show-server-actions";
|
||||
import { HandleServers } from "./handle-servers";
|
||||
import { SetupServer } from "./setup-server";
|
||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
||||
import { ShowMonitoringModal } from "./show-monitoring-modal";
|
||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||
@@ -314,6 +315,16 @@ export const ShowServers = () => {
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
{isCloud && (
|
||||
<ShowMonitoringModal
|
||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||
token={
|
||||
server?.metricsConfig?.server
|
||||
?.token
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ShowSwarmOverviewModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user