mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-27 08:45:33 +02:00
feat(cluster): implement advanced swarm settings forms
- Added multiple forms for managing swarm settings including Health Check, Restart Policy, Placement, Update Config, Rollback Config, Mode, Labels, Stop Grace Period, and Endpoint Spec. - Introduced utility functions for filtering empty values and checking for values to save. - Enhanced the UI for better navigation and form handling within the dashboard. - Integrated form validation using Zod and React Hook Form for improved user experience.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,151 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export const endpointSpecFormSchema = z.object({
|
||||||
|
Mode: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface EndpointSpecFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
resolver: zodResolver(endpointSpecFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
Mode: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.endpointSpecSwarm) {
|
||||||
|
const es = data.endpointSpecSwarm;
|
||||||
|
form.reset({
|
||||||
|
Mode: es.Mode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: z.infer<typeof endpointSpecFormSchema>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Check if all values are empty, if so, send null to clear the database
|
||||||
|
const hasAnyValue = formData.Mode !== undefined && formData.Mode !== null && formData.Mode !== "";
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
endpointSpecSwarm: hasAnyValue ? formData : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Endpoint spec updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating endpoint spec");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Mode"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Mode</FormLabel>
|
||||||
|
<FormDescription>Endpoint mode (vip or dnsrr)</FormDescription>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select endpoint mode" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="vip">VIP (Virtual IP)</SelectItem>
|
||||||
|
<SelectItem value="dnsrr">DNS Round Robin</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
Mode: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Endpoint Spec
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export const healthCheckFormSchema = z.object({
|
||||||
|
Test: z.array(z.string()).optional(),
|
||||||
|
Interval: z.coerce.number().optional(),
|
||||||
|
Timeout: z.coerce.number().optional(),
|
||||||
|
StartPeriod: z.coerce.number().optional(),
|
||||||
|
Retries: z.coerce.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface HealthCheckFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [testCommands, setTestCommands] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
resolver: zodResolver(healthCheckFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
Test: [],
|
||||||
|
Interval: undefined,
|
||||||
|
Timeout: undefined,
|
||||||
|
StartPeriod: undefined,
|
||||||
|
Retries: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.healthCheckSwarm) {
|
||||||
|
const hc = data.healthCheckSwarm;
|
||||||
|
form.reset({
|
||||||
|
Test: hc.Test || [],
|
||||||
|
Interval: hc.Interval,
|
||||||
|
Timeout: hc.Timeout,
|
||||||
|
StartPeriod: hc.StartPeriod,
|
||||||
|
Retries: hc.Retries,
|
||||||
|
});
|
||||||
|
setTestCommands(hc.Test || []);
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: z.infer<typeof healthCheckFormSchema>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Check if all values are empty, if so, send null to clear the database
|
||||||
|
const hasAnyValue =
|
||||||
|
(formData.Test && formData.Test.length > 0) ||
|
||||||
|
formData.Interval !== undefined ||
|
||||||
|
formData.Timeout !== undefined ||
|
||||||
|
formData.StartPeriod !== undefined ||
|
||||||
|
formData.Retries !== undefined;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
healthCheckSwarm: hasAnyValue ? formData : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Health check updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating health check");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTestCommand = () => {
|
||||||
|
setTestCommands([...testCommands, ""]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTestCommand = (index: number, value: string) => {
|
||||||
|
const newCommands = [...testCommands];
|
||||||
|
newCommands[index] = value;
|
||||||
|
setTestCommands(newCommands);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTestCommand = (index: number) => {
|
||||||
|
setTestCommands(testCommands.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<FormLabel>Test Commands</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Command to run for health check (e.g., ["CMD-SHELL", "curl -f
|
||||||
|
http://localhost:3000/health"])
|
||||||
|
</FormDescription>
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{testCommands.map((cmd, index) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={cmd}
|
||||||
|
onChange={(e) => updateTestCommand(index, e.target.value)}
|
||||||
|
placeholder={
|
||||||
|
index === 0
|
||||||
|
? "CMD-SHELL"
|
||||||
|
: "curl -f http://localhost:3000/health"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeTestCommand(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addTestCommand}
|
||||||
|
>
|
||||||
|
Add Command
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Interval"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Interval (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Time between health checks (e.g., 10000000000 for 10 seconds)
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Timeout"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Timeout (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum time to wait for health check response
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="StartPeriod"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Start Period (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Initial grace period before health checks begin
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Retries"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Retries</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Number of consecutive failures needed to consider container
|
||||||
|
unhealthy
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="3" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
Test: [],
|
||||||
|
Interval: undefined,
|
||||||
|
Timeout: undefined,
|
||||||
|
StartPeriod: undefined,
|
||||||
|
Retries: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Health Check
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export { HealthCheckForm } from "./health-check-form";
|
||||||
|
export { RestartPolicyForm } from "./restart-policy-form";
|
||||||
|
export { PlacementForm } from "./placement-form";
|
||||||
|
export { UpdateConfigForm } from "./update-config-form";
|
||||||
|
export { RollbackConfigForm } from "./rollback-config-form";
|
||||||
|
export { ModeForm } from "./mode-form";
|
||||||
|
export { LabelsForm } from "./labels-form";
|
||||||
|
export { StopGracePeriodForm } from "./stop-grace-period-form";
|
||||||
|
export { EndpointSpecForm } from "./endpoint-spec-form";
|
||||||
|
export { filterEmptyValues, hasValues } from "./utils";
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export const labelsFormSchema = z.object({
|
||||||
|
labels: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface LabelsFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
resolver: zodResolver(labelsFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
labels: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "labels",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.labelsSwarm && typeof data.labelsSwarm === "object") {
|
||||||
|
const labelEntries = Object.entries(data.labelsSwarm).map(
|
||||||
|
([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: value as string,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
form.reset({ labels: labelEntries });
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: z.infer<typeof labelsFormSchema>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const labelsObject =
|
||||||
|
formData.labels?.reduce(
|
||||||
|
(acc, { key, value }) => {
|
||||||
|
if (key && value) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
) || {};
|
||||||
|
|
||||||
|
// If no labels, send null to clear the database
|
||||||
|
const labelsToSend = Object.keys(labelsObject).length > 0 ? labelsObject : null;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
labelsSwarm: labelsToSend,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Labels updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating labels");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<FormLabel>Labels</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Add key-value labels to your service
|
||||||
|
</FormDescription>
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`labels.${index}.key`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="com.example.app.name" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`labels.${index}.value`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="my-app" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => append({ key: "", value: "" })}
|
||||||
|
>
|
||||||
|
Add Label
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({ labels: [] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Labels
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
interface ModeFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
defaultValues: {
|
||||||
|
type: undefined,
|
||||||
|
Replicas: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modeType = form.watch("type");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.modeSwarm) {
|
||||||
|
const mode = data.modeSwarm;
|
||||||
|
if (mode.Replicated) {
|
||||||
|
form.reset({
|
||||||
|
type: "Replicated",
|
||||||
|
Replicas: mode.Replicated.Replicas,
|
||||||
|
});
|
||||||
|
} else if (mode.Global) {
|
||||||
|
form.reset({
|
||||||
|
type: "Global",
|
||||||
|
Replicas: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: any) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// If no type is selected, send null to clear the database
|
||||||
|
if (!formData.type) {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
modeSwarm: null,
|
||||||
|
});
|
||||||
|
toast.success("Mode updated successfully");
|
||||||
|
refetch();
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modeData =
|
||||||
|
formData.type === "Replicated"
|
||||||
|
? { Replicated: { Replicas: formData.Replicas } }
|
||||||
|
: { Global: {} };
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
modeSwarm: modeData,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Mode updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating mode");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="type"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Mode Type</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Choose between replicated or global service mode
|
||||||
|
</FormDescription>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select mode type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Replicated">Replicated</SelectItem>
|
||||||
|
<SelectItem value="Global">Global</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{modeType === "Replicated" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Replicas"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Replicas</FormLabel>
|
||||||
|
<FormDescription>Number of replicas to run</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
type: undefined,
|
||||||
|
Replicas: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Mode
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
const PreferenceSchema = z.object({
|
||||||
|
Spread: z.object({
|
||||||
|
SpreadDescriptor: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PlatformSchema = z.object({
|
||||||
|
Architecture: z.string(),
|
||||||
|
OS: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const placementFormSchema = z.object({
|
||||||
|
Constraints: z.array(z.string()).optional(),
|
||||||
|
Preferences: z.array(PreferenceSchema).optional(),
|
||||||
|
MaxReplicas: z.coerce.number().optional(),
|
||||||
|
Platforms: z.array(PlatformSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PlacementFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
resolver: zodResolver(placementFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
Constraints: [],
|
||||||
|
Preferences: [],
|
||||||
|
MaxReplicas: undefined,
|
||||||
|
Platforms: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const constraints = form.watch("Constraints") || [];
|
||||||
|
const preferences = form.watch("Preferences") || [];
|
||||||
|
const platforms = form.watch("Platforms") || [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.placementSwarm) {
|
||||||
|
const placement = data.placementSwarm;
|
||||||
|
form.reset({
|
||||||
|
Constraints: placement.Constraints || [],
|
||||||
|
Preferences:
|
||||||
|
placement.Preferences?.map((p: any) => ({
|
||||||
|
SpreadDescriptor: p.Spread?.SpreadDescriptor || "",
|
||||||
|
})) || [],
|
||||||
|
MaxReplicas: placement.MaxReplicas,
|
||||||
|
Platforms: placement.Platforms || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: z.infer<typeof placementFormSchema>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Check if all values are empty, if so, send null to clear the database
|
||||||
|
const hasAnyValue =
|
||||||
|
(formData.Constraints && formData.Constraints.length > 0) ||
|
||||||
|
(formData.Preferences && formData.Preferences.length > 0) ||
|
||||||
|
(formData.Platforms && formData.Platforms.length > 0) ||
|
||||||
|
formData.MaxReplicas !== undefined;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
placementSwarm: hasAnyValue ? formData : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Placement updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating placement");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addConstraint = () => {
|
||||||
|
form.setValue("Constraints", [...constraints, ""]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConstraint = (index: number, value: string) => {
|
||||||
|
const newConstraints = [...constraints];
|
||||||
|
newConstraints[index] = value;
|
||||||
|
form.setValue("Constraints", newConstraints);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeConstraint = (index: number) => {
|
||||||
|
form.setValue(
|
||||||
|
"Constraints",
|
||||||
|
constraints.filter((_: string, i: number) => i !== index),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPreference = () => {
|
||||||
|
form.setValue("Preferences", [...preferences, { SpreadDescriptor: "" }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePreference = (index: number, value: string) => {
|
||||||
|
const newPreferences = [...preferences];
|
||||||
|
if (newPreferences[index]) {
|
||||||
|
newPreferences[index].SpreadDescriptor = value;
|
||||||
|
form.setValue("Preferences", newPreferences);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePreference = (index: number) => {
|
||||||
|
form.setValue(
|
||||||
|
"Preferences",
|
||||||
|
preferences.filter((_: any, i: number) => i !== index),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPlatform = () => {
|
||||||
|
form.setValue("Platforms", [...platforms, { Architecture: "", OS: "" }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePlatform = (
|
||||||
|
index: number,
|
||||||
|
field: "Architecture" | "OS",
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
const newPlatforms = [...platforms];
|
||||||
|
if (newPlatforms[index]) {
|
||||||
|
newPlatforms[index][field] = value;
|
||||||
|
form.setValue("Platforms", newPlatforms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePlatform = (index: number) => {
|
||||||
|
form.setValue(
|
||||||
|
"Platforms",
|
||||||
|
platforms.filter((_: any, i: number) => i !== index),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<FormLabel>Constraints</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Placement constraints (e.g., "node.role==manager")
|
||||||
|
</FormDescription>
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{constraints.map((constraint: string, index: number) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={constraint}
|
||||||
|
onChange={(e) => updateConstraint(index, e.target.value)}
|
||||||
|
placeholder="node.role==manager"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeConstraint(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addConstraint}
|
||||||
|
>
|
||||||
|
Add Constraint
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormLabel>Preferences</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Spread preferences for task distribution (e.g.,
|
||||||
|
"node.labels.region")
|
||||||
|
</FormDescription>
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{preferences.map((pref: any, index: number) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={pref.SpreadDescriptor}
|
||||||
|
onChange={(e) => updatePreference(index, e.target.value)}
|
||||||
|
placeholder="node.labels.region"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removePreference(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addPreference}
|
||||||
|
>
|
||||||
|
Add Preference
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="MaxReplicas"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Max Replicas</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum number of replicas per node
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormLabel>Platforms</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Target platforms for task scheduling
|
||||||
|
</FormDescription>
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{platforms.map((platform: any, index: number) => (
|
||||||
|
<div key={index} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={platform.Architecture}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePlatform(index, "Architecture", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="amd64"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={platform.OS}
|
||||||
|
onChange={(e) => updatePlatform(index, "OS", e.target.value)}
|
||||||
|
placeholder="linux"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removePlatform(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addPlatform}
|
||||||
|
>
|
||||||
|
Add Platform
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
Constraints: [],
|
||||||
|
Preferences: [],
|
||||||
|
MaxReplicas: undefined,
|
||||||
|
Platforms: [],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Placement
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export const restartPolicyFormSchema = z.object({
|
||||||
|
Condition: z.string().optional(),
|
||||||
|
Delay: z.coerce.number().optional(),
|
||||||
|
MaxAttempts: z.coerce.number().optional(),
|
||||||
|
Window: z.coerce.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface RestartPolicyFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
resolver: zodResolver(restartPolicyFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
Condition: undefined,
|
||||||
|
Delay: undefined,
|
||||||
|
MaxAttempts: undefined,
|
||||||
|
Window: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.restartPolicySwarm) {
|
||||||
|
form.reset({
|
||||||
|
Condition: data.restartPolicySwarm.Condition,
|
||||||
|
Delay: data.restartPolicySwarm.Delay,
|
||||||
|
MaxAttempts: data.restartPolicySwarm.MaxAttempts,
|
||||||
|
Window: data.restartPolicySwarm.Window,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (
|
||||||
|
formData: z.infer<typeof restartPolicyFormSchema>,
|
||||||
|
) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Check if all values are empty, if so, send null to clear the database
|
||||||
|
const hasAnyValue = Object.values(formData).some(
|
||||||
|
value => value !== undefined && value !== null && value !== ""
|
||||||
|
);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
restartPolicySwarm: hasAnyValue ? formData : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Restart policy updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating restart policy");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Condition"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Condition</FormLabel>
|
||||||
|
<FormDescription>When to restart the container</FormDescription>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select restart condition" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value="on-failure">On Failure</SelectItem>
|
||||||
|
<SelectItem value="any">Any</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Delay"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Delay (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Wait time between restart attempts
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="MaxAttempts"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Max Attempts</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum number of restart attempts
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="3" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Window"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Window (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Time window to evaluate restart policy
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
Condition: undefined,
|
||||||
|
Delay: undefined,
|
||||||
|
MaxAttempts: undefined,
|
||||||
|
Window: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Restart Policy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export const rollbackConfigFormSchema = z.object({
|
||||||
|
Parallelism: z.coerce.number().optional(),
|
||||||
|
Delay: z.coerce.number().optional(),
|
||||||
|
FailureAction: z.string().optional(),
|
||||||
|
Monitor: z.coerce.number().optional(),
|
||||||
|
MaxFailureRatio: z.coerce.number().optional(),
|
||||||
|
Order: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface RollbackConfigFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
resolver: zodResolver(rollbackConfigFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
Parallelism: undefined,
|
||||||
|
Delay: undefined,
|
||||||
|
FailureAction: undefined,
|
||||||
|
Monitor: undefined,
|
||||||
|
MaxFailureRatio: undefined,
|
||||||
|
Order: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.rollbackConfigSwarm) {
|
||||||
|
form.reset(data.rollbackConfigSwarm);
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (
|
||||||
|
formData: z.infer<typeof rollbackConfigFormSchema>,
|
||||||
|
) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Check if all values are empty, if so, send null to clear the database
|
||||||
|
const hasAnyValue = Object.values(formData).some(
|
||||||
|
value => value !== undefined && value !== null && value !== ""
|
||||||
|
);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
rollbackConfigSwarm: hasAnyValue ? formData : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Rollback config updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating rollback config");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Parallelism"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Parallelism</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Number of tasks to rollback simultaneously
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Delay"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Delay (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>Delay between task rollbacks</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="FailureAction"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Failure Action</FormLabel>
|
||||||
|
<FormDescription>Action on rollback failure</FormDescription>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select failure action" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="pause">Pause</SelectItem>
|
||||||
|
<SelectItem value="continue">Continue</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Monitor"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Monitor (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Duration to monitor for failure after rollback
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="MaxFailureRatio"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Max Failure Ratio</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum failure ratio tolerated (0-1)
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" step="0.01" placeholder="0.1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Order"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Order</FormLabel>
|
||||||
|
<FormDescription>Rollback order strategy</FormDescription>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select order" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="stop-first">Stop First</SelectItem>
|
||||||
|
<SelectItem value="start-first">Start First</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
Parallelism: undefined,
|
||||||
|
Delay: undefined,
|
||||||
|
FailureAction: undefined,
|
||||||
|
Monitor: undefined,
|
||||||
|
MaxFailureRatio: undefined,
|
||||||
|
Order: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Rollback Config
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
const hasStopGracePeriodSwarm = (
|
||||||
|
value: unknown,
|
||||||
|
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"stopGracePeriodSwarm" in value;
|
||||||
|
|
||||||
|
interface StopGracePeriodFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
defaultValues: {
|
||||||
|
value: null as bigint | null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasStopGracePeriodSwarm(data)) {
|
||||||
|
const value = data.stopGracePeriodSwarm;
|
||||||
|
const normalizedValue =
|
||||||
|
value === null || value === undefined
|
||||||
|
? null
|
||||||
|
: typeof value === "bigint"
|
||||||
|
? value
|
||||||
|
: BigInt(value);
|
||||||
|
form.reset({
|
||||||
|
value: normalizedValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: any) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
stopGracePeriodSwarm: formData.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Stop grace period updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating stop grace period");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="value"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Stop Grace Period (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Time to wait before forcefully killing the container
|
||||||
|
<br />
|
||||||
|
Examples: 30000000000 (30s), 120000000000 (2m)
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="30000000000"
|
||||||
|
{...field}
|
||||||
|
value={field?.value !== null && field?.value !== undefined ? field.value.toString() : ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(e.target.value ? BigInt(e.target.value) : null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
value: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Stop Grace Period
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export const updateConfigFormSchema = z.object({
|
||||||
|
Parallelism: z.coerce.number().optional(),
|
||||||
|
Delay: z.coerce.number().optional(),
|
||||||
|
FailureAction: z.string().optional(),
|
||||||
|
Monitor: z.coerce.number().optional(),
|
||||||
|
MaxFailureRatio: z.coerce.number().optional(),
|
||||||
|
Order: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UpdateConfigFormProps {
|
||||||
|
id: string;
|
||||||
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<any>({
|
||||||
|
resolver: zodResolver(updateConfigFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
Parallelism: undefined,
|
||||||
|
Delay: undefined,
|
||||||
|
FailureAction: undefined,
|
||||||
|
Monitor: undefined,
|
||||||
|
MaxFailureRatio: undefined,
|
||||||
|
Order: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.updateConfigSwarm) {
|
||||||
|
const config = data.updateConfigSwarm;
|
||||||
|
form.reset({
|
||||||
|
Parallelism: config.Parallelism,
|
||||||
|
Delay: config.Delay,
|
||||||
|
FailureAction: config.FailureAction,
|
||||||
|
Monitor: config.Monitor,
|
||||||
|
MaxFailureRatio: config.MaxFailureRatio,
|
||||||
|
Order: config.Order,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: z.infer<typeof updateConfigFormSchema>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Check if all values are empty, if so, send null to clear the database
|
||||||
|
const hasAnyValue = Object.values(formData).some(
|
||||||
|
value => value !== undefined && value !== null && value !== ""
|
||||||
|
);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
|
updateConfigSwarm: hasAnyValue ? formData : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Update config updated successfully");
|
||||||
|
refetch();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error updating update config");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Parallelism"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Parallelism</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Number of tasks to update simultaneously
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Delay"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Delay (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>Delay between task updates</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="FailureAction"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Failure Action</FormLabel>
|
||||||
|
<FormDescription>Action on update failure</FormDescription>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select failure action" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="pause">Pause</SelectItem>
|
||||||
|
<SelectItem value="continue">Continue</SelectItem>
|
||||||
|
<SelectItem value="rollback">Rollback</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Monitor"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Monitor (nanoseconds)</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Duration to monitor for failure after update
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="10000000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="MaxFailureRatio"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Max Failure Ratio</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum failure ratio tolerated (0-1)
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" step="0.01" placeholder="0.1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="Order"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Order</FormLabel>
|
||||||
|
<FormDescription>Update order strategy</FormDescription>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select order" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="stop-first">Stop First</SelectItem>
|
||||||
|
<SelectItem value="start-first">Start First</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
Parallelism: undefined,
|
||||||
|
Delay: undefined,
|
||||||
|
FailureAction: undefined,
|
||||||
|
Monitor: undefined,
|
||||||
|
MaxFailureRatio: undefined,
|
||||||
|
Order: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
Save Update Config
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Filters out undefined, null, and empty string values from form data
|
||||||
|
* Only returns fields that have actual values
|
||||||
|
*/
|
||||||
|
export const filterEmptyValues = (formData: Record<string, any>): Record<string, any> => {
|
||||||
|
return Object.entries(formData).reduce((acc, [key, value]) => {
|
||||||
|
// Keep arrays even if empty (they might be intentionally cleared)
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length > 0) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For other values, filter out undefined, null, and empty strings
|
||||||
|
else if (value !== undefined && value !== null && value !== "") {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any>);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if filtered data has any values to save
|
||||||
|
*/
|
||||||
|
export const hasValues = (data: Record<string, any>): boolean => {
|
||||||
|
return Object.keys(data).length > 0;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user