Update websocket to be consistant with streaming

This commit is contained in:
Owen
2026-04-16 21:27:06 -07:00
parent f932cc7aca
commit 3645cc5759
8 changed files with 275 additions and 49 deletions

View File

@@ -30,6 +30,11 @@ import {
SelectTrigger,
SelectValue
} from "@app/components/ui/select";
import {
RadioGroup,
RadioGroupItem
} from "@app/components/ui/radio-group";
import { Label } from "@app/components/ui/label";
import { TagInput, type Tag } from "@app/components/tags/tag-input";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import {
@@ -322,8 +327,12 @@ export function ActionBlock({
type: "webhook",
url: "",
method: "POST",
headers: [{ key: "", value: "" }],
secret: ""
headers: [],
authType: "none",
bearerToken: "",
basicCredentials: "",
customHeaderName: "",
customHeaderValue: ""
});
}
}}
@@ -580,26 +589,187 @@ function WebhookActionFields({
</FormItem>
)}
/>
<FormField
control={control}
name={`actions.${index}.secret`}
render={({ field }) => (
<FormItem>
<FormLabel>{t("alertingWebhookSecret")}</FormLabel>
<FormControl>
<Input
{...field}
type="password"
autoComplete="new-password"
placeholder={t(
"alertingWebhookSecretPlaceholder"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Authentication */}
<div className="space-y-3">
<div>
<label className="font-medium text-sm block">
{t("httpDestAuthTitle")}
</label>
<p className="text-xs text-muted-foreground mt-0.5">
{t("httpDestAuthDescription")}
</p>
</div>
<FormField
control={control}
name={`actions.${index}.authType`}
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="gap-2"
>
{/* None */}
<div className="flex items-start gap-3 rounded-md border p-3 transition-colors">
<RadioGroupItem
value="none"
id={`auth-none-${index}`}
className="mt-0.5"
/>
<div>
<Label
htmlFor={`auth-none-${index}`}
className="cursor-pointer font-medium"
>
{t("httpDestAuthNoneTitle")}
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
{t("httpDestAuthNoneDescription")}
</p>
</div>
</div>
{/* Bearer */}
<div className="flex items-start gap-3 rounded-md border p-3">
<RadioGroupItem
value="bearer"
id={`auth-bearer-${index}`}
className="mt-0.5"
/>
<div className="flex-1 space-y-3">
<div>
<Label
htmlFor={`auth-bearer-${index}`}
className="cursor-pointer font-medium"
>
{t("httpDestAuthBearerTitle")}
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
{t("httpDestAuthBearerDescription")}
</p>
</div>
{field.value === "bearer" && (
<FormField
control={control}
name={`actions.${index}.bearerToken`}
render={({ field: f }) => (
<FormItem>
<FormControl>
<Input
{...f}
placeholder={t("httpDestAuthBearerPlaceholder")}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</div>
{/* Basic */}
<div className="flex items-start gap-3 rounded-md border p-3">
<RadioGroupItem
value="basic"
id={`auth-basic-${index}`}
className="mt-0.5"
/>
<div className="flex-1 space-y-3">
<div>
<Label
htmlFor={`auth-basic-${index}`}
className="cursor-pointer font-medium"
>
{t("httpDestAuthBasicTitle")}
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
{t("httpDestAuthBasicDescription")}
</p>
</div>
{field.value === "basic" && (
<FormField
control={control}
name={`actions.${index}.basicCredentials`}
render={({ field: f }) => (
<FormItem>
<FormControl>
<Input
{...f}
placeholder={t("httpDestAuthBasicPlaceholder")}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</div>
{/* Custom */}
<div className="flex items-start gap-3 rounded-md border p-3">
<RadioGroupItem
value="custom"
id={`auth-custom-${index}`}
className="mt-0.5"
/>
<div className="flex-1 space-y-3">
<div>
<Label
htmlFor={`auth-custom-${index}`}
className="cursor-pointer font-medium"
>
{t("httpDestAuthCustomTitle")}
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
{t("httpDestAuthCustomDescription")}
</p>
</div>
{field.value === "custom" && (
<div className="flex gap-2">
<FormField
control={control}
name={`actions.${index}.customHeaderName`}
render={({ field: f }) => (
<FormItem className="flex-1">
<FormControl>
<Input
{...f}
placeholder={t("httpDestAuthCustomHeaderNamePlaceholder")}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`actions.${index}.customHeaderValue`}
render={({ field: f }) => (
<FormItem className="flex-1">
<FormControl>
<Input
{...f}
placeholder={t("httpDestAuthCustomHeaderValuePlaceholder")}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</div>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<WebhookHeadersField index={index} control={control} form={form} />
</div>
);

View File

@@ -34,7 +34,11 @@ export type AlertRuleFormAction =
url: string;
method: string;
headers: { key: string; value: string }[];
secret: string;
authType: "none" | "bearer" | "basic" | "custom";
bearerToken: string;
basicCredentials: string;
customHeaderName: string;
customHeaderValue: string;
};
export type AlertRuleFormValues = {
@@ -95,6 +99,15 @@ export type AlertRuleApiResponse = {
webhookUrl: string;
enabled: boolean;
lastSentAt: number | null;
config: {
authType: string;
bearerToken?: string;
basicCredentials?: string;
customHeaderName?: string;
customHeaderValue?: string;
headers?: { key: string; value: string }[];
method?: string;
} | null;
}[];
};
@@ -155,7 +168,11 @@ export function buildFormSchema(t: (k: string) => string) {
value: z.string()
})
),
secret: z.string()
authType: z.enum(["none", "bearer", "basic", "custom"]),
bearerToken: z.string(),
basicCredentials: z.string(),
customHeaderName: z.string(),
customHeaderValue: z.string()
})
])
)
@@ -293,12 +310,19 @@ export function apiResponseToFormValues(
// Each webhook action becomes its own form webhook action
for (const w of rule.webhookActions) {
const cfg = w.config;
actions.push({
type: "webhook",
url: w.webhookUrl,
method: "POST",
headers: [{ key: "", value: "" }],
secret: ""
method: cfg?.method ?? "POST",
headers: cfg?.headers?.length
? cfg.headers
: [{ key: "", value: "" }],
authType: (cfg?.authType as "none" | "bearer" | "basic" | "custom") ?? "none",
bearerToken: cfg?.bearerToken ?? "",
basicCredentials: cfg?.basicCredentials ?? "",
customHeaderName: cfg?.customHeaderName ?? "",
customHeaderValue: cfg?.customHeaderValue ?? ""
});
}
@@ -352,18 +376,15 @@ export function formValuesToApiPayload(
webhookActions.push({
webhookUrl: action.url.trim(),
enabled: true,
// Encode any headers / secret as config JSON if present
...(action.secret.trim() ||
action.headers.some((h) => h.key.trim())
? {
config: JSON.stringify({
secret: action.secret.trim() || undefined,
headers: action.headers.filter(
(h) => h.key.trim()
)
})
}
: {})
config: JSON.stringify({
authType: action.authType,
bearerToken: action.bearerToken || undefined,
basicCredentials: action.basicCredentials || undefined,
customHeaderName: action.customHeaderName || undefined,
customHeaderValue: action.customHeaderValue || undefined,
headers: action.headers.filter((h) => h.key.trim()),
method: action.method
})
});
}
}

View File

@@ -28,7 +28,7 @@ import z from "zod";
import { remote } from "./api";
import { durationToMs } from "./durationToMs";
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
import { StatusHistoryResponse } from "@server/middlewares/statusHistory";
import { StatusHistoryResponse } from "@server/lib/statusHistory";
export type ProductUpdate = {
link: string | null;