Translate siem

This commit is contained in:
Owen
2026-03-31 15:09:14 -07:00
parent c1bd36231d
commit 3b8dd45a73
3 changed files with 224 additions and 200 deletions

View File

@@ -2804,5 +2804,89 @@
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review", "approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
"approvalsEmptyStateButtonText": "Manage Roles", "approvalsEmptyStateButtonText": "Manage Roles",
"domainErrorTitle": "We are having trouble verifying your domain", "domainErrorTitle": "We are having trouble verifying your domain",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab." "idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
"streamingTitle": "Event Streaming",
"streamingDescription": "Stream events from your organization to external destinations in real time.",
"streamingUnnamedDestination": "Unnamed destination",
"streamingNoUrlConfigured": "No URL configured",
"streamingAddDestination": "Add Destination",
"streamingHttpWebhookTitle": "HTTP Webhook",
"streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.",
"streamingS3Title": "Amazon S3",
"streamingS3Description": "Stream events to an S3-compatible object storage bucket. Coming soon.",
"streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "Forward events directly to your Datadog account. Coming soon.",
"streamingTypePickerDescription": "Choose a destination type to get started.",
"streamingFailedToLoad": "Failed to load destinations",
"streamingUnexpectedError": "An unexpected error occurred.",
"streamingFailedToUpdate": "Failed to update destination",
"streamingDeletedSuccess": "Destination deleted successfully",
"streamingFailedToDelete": "Failed to delete destination",
"streamingDeleteTitle": "Delete Destination",
"streamingDeleteButtonText": "Delete Destination",
"streamingDeleteDialogAreYouSure": "Are you sure you want to delete",
"streamingDeleteDialogThisDestination": "this destination",
"streamingDeleteDialogPermanentlyRemoved": "? All configuration will be permanently removed.",
"httpDestEditTitle": "Edit Destination",
"httpDestAddTitle": "Add HTTP Destination",
"httpDestEditDescription": "Update the configuration for this HTTP event streaming destination.",
"httpDestAddDescription": "Configure a new HTTP endpoint to receive your organization's events.",
"httpDestTabSettings": "Settings",
"httpDestTabHeaders": "Headers",
"httpDestTabBody": "Body",
"httpDestTabLogs": "Logs",
"httpDestNamePlaceholder": "My HTTP destination",
"httpDestUrlLabel": "Destination URL",
"httpDestUrlErrorHttpRequired": "URL must use http or https",
"httpDestUrlErrorHttpsRequired": "HTTPS is required on cloud deployments",
"httpDestUrlErrorInvalid": "Enter a valid URL (e.g. https://example.com/webhook)",
"httpDestAuthTitle": "Authentication",
"httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.",
"httpDestAuthNoneTitle": "No Authentication",
"httpDestAuthNoneDescription": "Sends requests without an Authorization header.",
"httpDestAuthBearerTitle": "Bearer Token",
"httpDestAuthBearerDescription": "Adds an Authorization: Bearer <token> header to each request.",
"httpDestAuthBearerPlaceholder": "Your API key or token",
"httpDestAuthBasicTitle": "Basic Auth",
"httpDestAuthBasicDescription": "Adds an Authorization: Basic <credentials> header. Provide credentials as username:password.",
"httpDestAuthBasicPlaceholder": "username:password",
"httpDestAuthCustomTitle": "Custom Header",
"httpDestAuthCustomDescription": "Specify a custom HTTP header name and value for authentication (e.g. X-API-Key).",
"httpDestAuthCustomHeaderNamePlaceholder": "Header name (e.g. X-API-Key)",
"httpDestAuthCustomHeaderValuePlaceholder": "Header value",
"httpDestCustomHeadersTitle": "Custom HTTP Headers",
"httpDestCustomHeadersDescription": "Add custom headers to every outgoing request. Useful for static tokens or a custom Content-Type. By default, Content-Type: application/json is sent.",
"httpDestNoHeadersConfigured": "No custom headers configured. Click \"Add Header\" to add one.",
"httpDestHeaderNamePlaceholder": "Header name",
"httpDestHeaderValuePlaceholder": "Value",
"httpDestAddHeader": "Add Header",
"httpDestBodyTemplateTitle": "Custom Body Template",
"httpDestBodyTemplateDescription": "Control the JSON payload structure sent to your endpoint. If disabled, a default JSON object is sent for each event.",
"httpDestEnableBodyTemplate": "Enable custom body template",
"httpDestBodyTemplateLabel": "Body Template (JSON)",
"httpDestBodyTemplateHint": "Use template variables to reference event fields in your payload.",
"httpDestPayloadFormatTitle": "Payload Format",
"httpDestPayloadFormatDescription": "How events are serialised into each request body.",
"httpDestFormatJsonArrayTitle": "JSON Array",
"httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.",
"httpDestFormatNdjsonTitle": "NDJSON",
"httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON — one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.",
"httpDestFormatSingleTitle": "One Event Per Request",
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
"httpDestLogTypesTitle": "Log Types",
"httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.",
"httpDestAccessLogsTitle": "Access Logs",
"httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.",
"httpDestActionLogsTitle": "Action Logs",
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
"httpDestConnectionLogsTitle": "Connection Logs",
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
"httpDestRequestLogsTitle": "Request Logs",
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
"httpDestSaveChanges": "Save Changes",
"httpDestCreateDestination": "Create Destination",
"httpDestUpdatedSuccess": "Destination updated successfully",
"httpDestCreatedSuccess": "Destination created successfully",
"httpDestUpdateFailed": "Failed to update destination",
"httpDestCreateFailed": "Failed to create destination"
} }

View File

@@ -38,6 +38,7 @@ import {
HttpDestinationCredenza, HttpDestinationCredenza,
parseHttpConfig parseHttpConfig
} from "@app/components/HttpDestinationCredenza"; } from "@app/components/HttpDestinationCredenza";
import { useTranslations } from "next-intl";
// ── Re-export Destination so the rest of the file can use it ────────────────── // ── Re-export Destination so the rest of the file can use it ──────────────────
@@ -69,6 +70,7 @@ function DestinationCard({
isToggling, isToggling,
disabled = false disabled = false
}: DestinationCardProps) { }: DestinationCardProps) {
const t = useTranslations();
const cfg = parseHttpConfig(destination.config); const cfg = parseHttpConfig(destination.config);
return ( return (
@@ -84,7 +86,7 @@ function DestinationCard({
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="font-semibold text-sm leading-tight truncate"> <p className="font-semibold text-sm leading-tight truncate">
{cfg.name || "Unnamed destination"} {cfg.name || t("streamingUnnamedDestination")}
</p> </p>
<p className="text-xs text-muted-foreground truncate mt-0.5"> <p className="text-xs text-muted-foreground truncate mt-0.5">
HTTP HTTP
@@ -104,7 +106,7 @@ function DestinationCard({
{/* URL preview */} {/* URL preview */}
<p className="text-xs text-muted-foreground truncate"> <p className="text-xs text-muted-foreground truncate">
{cfg.url || ( {cfg.url || (
<span className="italic">No URL configured</span> <span className="italic">{t("streamingNoUrlConfigured")}</span>
)} )}
</p> </p>
@@ -116,7 +118,7 @@ function DestinationCard({
disabled={disabled} disabled={disabled}
className="flex-1" className="flex-1"
> >
Edit {t("edit")}
</Button> </Button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -134,7 +136,7 @@ function DestinationCard({
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
onClick={() => onDelete(destination)} onClick={() => onDelete(destination)}
> >
Delete {t("delete")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -146,6 +148,8 @@ function DestinationCard({
// ── Add destination card ─────────────────────────────────────────────────────── // ── Add destination card ───────────────────────────────────────────────────────
function AddDestinationCard({ onClick }: { onClick: () => void }) { function AddDestinationCard({ onClick }: { onClick: () => void }) {
const t = useTranslations();
return ( return (
<button <button
type="button" type="button"
@@ -156,7 +160,7 @@ function AddDestinationCard({ onClick }: { onClick: () => void }) {
<div className="flex items-center justify-center w-9 h-9 rounded-md border-2 border-dashed border-current"> <div className="flex items-center justify-center w-9 h-9 rounded-md border-2 border-dashed border-current">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</div> </div>
<span className="text-sm font-medium">Add Destination</span> <span className="text-sm font-medium">{t("streamingAddDestination")}</span>
</div> </div>
</button> </button>
); );
@@ -166,48 +170,6 @@ function AddDestinationCard({ onClick }: { onClick: () => void }) {
type DestinationType = "http" | "s3" | "datadog"; type DestinationType = "http" | "s3" | "datadog";
const destinationTypeOptions: ReadonlyArray<StrategyOption<DestinationType>> = [
{
id: "http",
title: "HTTP Webhook",
description:
"Send events to any HTTP endpoint with flexible authentication and templating.",
icon: <Globe className="h-6 w-6" />
},
{
id: "s3",
title: "Amazon S3",
description:
"Stream events to an S3-compatible object storage bucket. Coming soon.",
disabled: true,
icon: (
<Image
src="/third-party/s3.png"
alt="Amazon S3"
width={24}
height={24}
className="rounded-sm"
/>
)
},
{
id: "datadog",
title: "Datadog",
description:
"Forward events directly to your Datadog account. Coming soon.",
disabled: true,
icon: (
<Image
src="/third-party/dd.png"
alt="Datadog"
width={24}
height={24}
className="rounded-sm"
/>
)
}
];
interface DestinationTypePickerProps { interface DestinationTypePickerProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@@ -221,8 +183,48 @@ function DestinationTypePicker({
onSelect, onSelect,
isPaywalled = false isPaywalled = false
}: DestinationTypePickerProps) { }: DestinationTypePickerProps) {
const t = useTranslations();
const [selected, setSelected] = useState<DestinationType>("http"); const [selected, setSelected] = useState<DestinationType>("http");
const destinationTypeOptions: ReadonlyArray<StrategyOption<DestinationType>> = [
{
id: "http",
title: t("streamingHttpWebhookTitle"),
description: t("streamingHttpWebhookDescription"),
icon: <Globe className="h-6 w-6" />
},
{
id: "s3",
title: t("streamingS3Title"),
description: t("streamingS3Description"),
disabled: true,
icon: (
<Image
src="/third-party/s3.png"
alt={t("streamingS3Title")}
width={24}
height={24}
className="rounded-sm"
/>
)
},
{
id: "datadog",
title: t("streamingDatadogTitle"),
description: t("streamingDatadogDescription"),
disabled: true,
icon: (
<Image
src="/third-party/dd.png"
alt={t("streamingDatadogTitle")}
width={24}
height={24}
className="rounded-sm"
/>
)
}
];
useEffect(() => { useEffect(() => {
if (open) setSelected("http"); if (open) setSelected("http");
}, [open]); }, [open]);
@@ -231,9 +233,9 @@ function DestinationTypePicker({
<Credenza open={open} onOpenChange={onOpenChange}> <Credenza open={open} onOpenChange={onOpenChange}>
<CredenzaContent className="sm:max-w-lg"> <CredenzaContent className="sm:max-w-lg">
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle>Add Destination</CredenzaTitle> <CredenzaTitle>{t("streamingAddDestination")}</CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
Choose a destination type to get started. {t("streamingTypePickerDescription")}
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
@@ -248,13 +250,13 @@ function DestinationTypePicker({
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline">Cancel</Button> <Button variant="outline">{t("cancel")}</Button>
</CredenzaClose> </CredenzaClose>
<Button <Button
onClick={() => onSelect(selected)} onClick={() => onSelect(selected)}
disabled={isPaywalled} disabled={isPaywalled}
> >
Continue {t("continue")}
</Button> </Button>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
@@ -269,6 +271,7 @@ export default function StreamingDestinationsPage() {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const isEnterprise = isPaidUser(tierMatrix[TierFeature.SIEM]); const isEnterprise = isPaidUser(tierMatrix[TierFeature.SIEM]);
const t = useTranslations();
const [destinations, setDestinations] = useState<Destination[]>([]); const [destinations, setDestinations] = useState<Destination[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -297,10 +300,10 @@ export default function StreamingDestinationsPage() {
} catch (e) { } catch (e) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Failed to load destinations", title: t("streamingFailedToLoad"),
description: formatAxiosError( description: formatAxiosError(
e, e,
"An unexpected error occurred." t("streamingUnexpectedError")
) )
}); });
} finally { } finally {
@@ -337,10 +340,10 @@ export default function StreamingDestinationsPage() {
); );
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Failed to update destination", title: t("streamingFailedToUpdate"),
description: formatAxiosError( description: formatAxiosError(
e, e,
"An unexpected error occurred." t("streamingUnexpectedError")
) )
}); });
} finally { } finally {
@@ -364,17 +367,17 @@ export default function StreamingDestinationsPage() {
await api.delete( await api.delete(
`/org/${orgId}/event-streaming-destination/${deleteTarget.destinationId}` `/org/${orgId}/event-streaming-destination/${deleteTarget.destinationId}`
); );
toast({ title: "Destination deleted successfully" }); toast({ title: t("streamingDeletedSuccess") });
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setDeleteTarget(null); setDeleteTarget(null);
loadDestinations(); loadDestinations();
} catch (e) { } catch (e) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Failed to delete destination", title: t("streamingFailedToDelete"),
description: formatAxiosError( description: formatAxiosError(
e, e,
"An unexpected error occurred." t("streamingUnexpectedError")
) )
}); });
} finally { } finally {
@@ -400,8 +403,8 @@ export default function StreamingDestinationsPage() {
return ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
title="Event Streaming" title={t("streamingTitle")}
description="Stream events from your organization to external destinations in real time." description={t("streamingDescription")}
/> />
<PaidFeaturesAlert tiers={tierMatrix[TierFeature.SIEM]} /> <PaidFeaturesAlert tiers={tierMatrix[TierFeature.SIEM]} />
@@ -456,20 +459,20 @@ export default function StreamingDestinationsPage() {
if (!v) setDeleteTarget(null); if (!v) setDeleteTarget(null);
}} }}
string={ string={
parseHttpConfig(deleteTarget.config).name || "delete" parseHttpConfig(deleteTarget.config).name || t("streamingDeleteDialogThisDestination")
} }
title="Delete Destination" title={t("streamingDeleteTitle")}
dialog={ dialog={
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Are you sure you want to delete{" "} {t("streamingDeleteDialogAreYouSure")}{" "}
<span className="font-semibold text-foreground"> <span className="font-semibold text-foreground">
{parseHttpConfig(deleteTarget.config).name || {parseHttpConfig(deleteTarget.config).name ||
"this destination"} t("streamingDeleteDialogThisDestination")}
</span> </span>
? All configuration will be permanently removed. {t("streamingDeleteDialogPermanentlyRemoved")}
</p> </p>
} }
buttonText="Delete Destination" buttonText={t("streamingDeleteButtonText")}
onConfirm={handleDeleteConfirm} onConfirm={handleDeleteConfirm}
/> />
)} )}

View File

@@ -24,6 +24,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { build } from "@server/build"; import { build } from "@server/build";
import { useTranslations } from "next-intl";
// ── Types ────────────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────────────
@@ -91,6 +92,8 @@ interface HeadersEditorProps {
} }
function HeadersEditor({ headers, onChange }: HeadersEditorProps) { function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
const t = useTranslations();
const addRow = () => onChange([...headers, { key: "", value: "" }]); const addRow = () => onChange([...headers, { key: "", value: "" }]);
const removeRow = (i: number) => const removeRow = (i: number) =>
@@ -106,8 +109,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
<div className="space-y-3"> <div className="space-y-3">
{headers.length === 0 && ( {headers.length === 0 && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
No custom headers configured. Click "Add Header" to add {t("httpDestNoHeadersConfigured")}
one.
</p> </p>
)} )}
{headers.map((h, i) => ( {headers.map((h, i) => (
@@ -115,7 +117,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
<Input <Input
value={h.key} value={h.key}
onChange={(e) => updateRow(i, "key", e.target.value)} onChange={(e) => updateRow(i, "key", e.target.value)}
placeholder="Header name" placeholder={t("httpDestHeaderNamePlaceholder")}
className="flex-1" className="flex-1"
/> />
<Input <Input
@@ -123,7 +125,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
onChange={(e) => onChange={(e) =>
updateRow(i, "value", e.target.value) updateRow(i, "value", e.target.value)
} }
placeholder="Value" placeholder={t("httpDestHeaderValuePlaceholder")}
className="flex-1" className="flex-1"
/> />
<Button <Button
@@ -145,7 +147,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
className="gap-1.5" className="gap-1.5"
> >
<Plus className="h-3.5 w-3.5" /> <Plus className="h-3.5 w-3.5" />
Add Header {t("httpDestAddHeader")}
</Button> </Button>
</div> </div>
); );
@@ -169,6 +171,7 @@ export function HttpDestinationCredenza({
onSaved onSaved
}: HttpDestinationCredenzaProps) { }: HttpDestinationCredenzaProps) {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations();
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [cfg, setCfg] = useState<HttpConfig>(defaultHttpConfig()); const [cfg, setCfg] = useState<HttpConfig>(defaultHttpConfig());
@@ -201,14 +204,14 @@ export function HttpDestinationCredenza({
parsed.protocol !== "http:" && parsed.protocol !== "http:" &&
parsed.protocol !== "https:" parsed.protocol !== "https:"
) { ) {
return "URL must use http or https"; return t("httpDestUrlErrorHttpRequired");
} }
if (build === "saas" && parsed.protocol !== "https:") { if (build === "saas" && parsed.protocol !== "https:") {
return "HTTPS is required on cloud deployments"; return t("httpDestUrlErrorHttpsRequired");
} }
return null; return null;
} catch { } catch {
return "Enter a valid URL (e.g. https://example.com/webhook)"; return t("httpDestUrlErrorInvalid");
} }
})(); })();
@@ -234,13 +237,13 @@ export function HttpDestinationCredenza({
`/org/${orgId}/event-streaming-destination/${editing.destinationId}`, `/org/${orgId}/event-streaming-destination/${editing.destinationId}`,
payload payload
); );
toast({ title: "Destination updated successfully" }); toast({ title: t("httpDestUpdatedSuccess") });
} else { } else {
await api.put( await api.put(
`/org/${orgId}/event-streaming-destination`, `/org/${orgId}/event-streaming-destination`,
payload payload
); );
toast({ title: "Destination created successfully" }); toast({ title: t("httpDestCreatedSuccess") });
} }
onSaved(); onSaved();
onOpenChange(false); onOpenChange(false);
@@ -248,11 +251,11 @@ export function HttpDestinationCredenza({
toast({ toast({
variant: "destructive", variant: "destructive",
title: editing title: editing
? "Failed to update destination" ? t("httpDestUpdateFailed")
: "Failed to create destination", : t("httpDestCreateFailed"),
description: formatAxiosError( description: formatAxiosError(
e, e,
"An unexpected error occurred." t("streamingUnexpectedError")
) )
}); });
} finally { } finally {
@@ -266,13 +269,13 @@ export function HttpDestinationCredenza({
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle> <CredenzaTitle>
{editing {editing
? "Edit Destination" ? t("httpDestEditTitle")
: "Add HTTP Destination"} : t("httpDestAddTitle")}
</CredenzaTitle> </CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
{editing {editing
? "Update the configuration for this HTTP event streaming destination." ? t("httpDestEditDescription")
: "Configure a new HTTP endpoint to receive your organization's events."} : t("httpDestAddDescription")}
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
@@ -280,20 +283,20 @@ export function HttpDestinationCredenza({
<HorizontalTabs <HorizontalTabs
clientSide clientSide
items={[ items={[
{ title: "Settings", href: "" }, { title: t("httpDestTabSettings"), href: "" },
{ title: "Headers", href: "" }, { title: t("httpDestTabHeaders"), href: "" },
{ title: "Body", href: "" }, { title: t("httpDestTabBody"), href: "" },
{ title: "Logs", href: "" } { title: t("httpDestTabLogs"), href: "" }
]} ]}
> >
{/* ── Settings tab ────────────────────────────── */} {/* ── Settings tab ────────────────────────────── */}
<div className="space-y-6 mt-4 p-1"> <div className="space-y-6 mt-4 p-1">
{/* Name */} {/* Name */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="dest-name">Name</Label> <Label htmlFor="dest-name">{t("name")}</Label>
<Input <Input
id="dest-name" id="dest-name"
placeholder="My HTTP destination" placeholder={t("httpDestNamePlaceholder")}
value={cfg.name} value={cfg.name}
onChange={(e) => onChange={(e) =>
update({ name: e.target.value }) update({ name: e.target.value })
@@ -304,7 +307,7 @@ export function HttpDestinationCredenza({
{/* URL */} {/* URL */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="dest-url"> <Label htmlFor="dest-url">
Destination URL {t("httpDestUrlLabel")}
</Label> </Label>
<Input <Input
id="dest-url" id="dest-url"
@@ -325,11 +328,10 @@ export function HttpDestinationCredenza({
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="font-medium block"> <label className="font-medium block">
Authentication {t("httpDestAuthTitle")}
</label> </label>
<p className="text-sm text-muted-foreground mt-0.5"> <p className="text-sm text-muted-foreground mt-0.5">
Choose how requests to your endpoint {t("httpDestAuthDescription")}
are authenticated.
</p> </p>
</div> </div>
@@ -352,14 +354,10 @@ export function HttpDestinationCredenza({
htmlFor="auth-none" htmlFor="auth-none"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
No Authentication {t("httpDestAuthNoneTitle")}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Sends requests without an{" "} {t("httpDestAuthNoneDescription")}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
Authorization
</code>{" "}
header.
</p> </p>
</div> </div>
</div> </div>
@@ -377,20 +375,15 @@ export function HttpDestinationCredenza({
htmlFor="auth-bearer" htmlFor="auth-bearer"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
Bearer Token {t("httpDestAuthBearerTitle")}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Adds an{" "} {t("httpDestAuthBearerDescription")}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
Authorization: Bearer
&lt;token&gt;
</code>{" "}
header to each request.
</p> </p>
</div> </div>
{cfg.authType === "bearer" && ( {cfg.authType === "bearer" && (
<Input <Input
placeholder="Your API key or token" placeholder={t("httpDestAuthBearerPlaceholder")}
value={ value={
cfg.bearerToken ?? "" cfg.bearerToken ?? ""
} }
@@ -418,25 +411,15 @@ export function HttpDestinationCredenza({
htmlFor="auth-basic" htmlFor="auth-basic"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
Basic Auth {t("httpDestAuthBasicTitle")}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Adds an{" "} {t("httpDestAuthBasicDescription")}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
Authorization: Basic
&lt;credentials&gt;
</code>{" "}
header. Provide credentials
as{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
username:password
</code>
.
</p> </p>
</div> </div>
{cfg.authType === "basic" && ( {cfg.authType === "basic" && (
<Input <Input
placeholder="username:password" placeholder={t("httpDestAuthBasicPlaceholder")}
value={ value={
cfg.basicCredentials ?? cfg.basicCredentials ??
"" ""
@@ -465,22 +448,16 @@ export function HttpDestinationCredenza({
htmlFor="auth-custom" htmlFor="auth-custom"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
Custom Header {t("httpDestAuthCustomTitle")}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Specify a custom HTTP {t("httpDestAuthCustomDescription")}
header name and value for
authentication (e.g.{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
X-API-Key
</code>
).
</p> </p>
</div> </div>
{cfg.authType === "custom" && ( {cfg.authType === "custom" && (
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
placeholder="Header name (e.g. X-API-Key)" placeholder={t("httpDestAuthCustomHeaderNamePlaceholder")}
value={ value={
cfg.customHeaderName ?? cfg.customHeaderName ??
"" ""
@@ -495,7 +472,7 @@ export function HttpDestinationCredenza({
className="flex-1" className="flex-1"
/> />
<Input <Input
placeholder="Header value" placeholder={t("httpDestAuthCustomHeaderValuePlaceholder")}
value={ value={
cfg.customHeaderValue ?? cfg.customHeaderValue ??
"" ""
@@ -521,20 +498,10 @@ export function HttpDestinationCredenza({
<div className="space-y-6 mt-4 p-1"> <div className="space-y-6 mt-4 p-1">
<div> <div>
<label className="font-medium block"> <label className="font-medium block">
Custom HTTP Headers {t("httpDestCustomHeadersTitle")}
</label> </label>
<p className="text-sm text-muted-foreground mt-0.5"> <p className="text-sm text-muted-foreground mt-0.5">
Add custom headers to every outgoing {t("httpDestCustomHeadersDescription")}
request. Useful for static tokens or
custom{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
Content-Type
</code>
. By default,{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
Content-Type: application/json
</code>{" "}
is sent.
</p> </p>
</div> </div>
<HeadersEditor <HeadersEditor
@@ -547,12 +514,10 @@ export function HttpDestinationCredenza({
<div className="space-y-6 mt-4 p-1"> <div className="space-y-6 mt-4 p-1">
<div> <div>
<label className="font-medium block"> <label className="font-medium block">
Custom Body Template {t("httpDestBodyTemplateTitle")}
</label> </label>
<p className="text-sm text-muted-foreground mt-0.5"> <p className="text-sm text-muted-foreground mt-0.5">
Control the JSON payload structure sent to {t("httpDestBodyTemplateDescription")}
your endpoint. If disabled, a default JSON
object is sent for each event.
</p> </p>
</div> </div>
@@ -568,14 +533,14 @@ export function HttpDestinationCredenza({
htmlFor="use-body-template" htmlFor="use-body-template"
className="cursor-pointer" className="cursor-pointer"
> >
Enable custom body template {t("httpDestEnableBodyTemplate")}
</Label> </Label>
</div> </div>
{cfg.useBodyTemplate && ( {cfg.useBodyTemplate && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="body-template"> <Label htmlFor="body-template">
Body Template (JSON) {t("httpDestBodyTemplateLabel")}
</Label> </Label>
<Textarea <Textarea
id="body-template" id="body-template"
@@ -591,8 +556,7 @@ export function HttpDestinationCredenza({
className="font-mono text-xs min-h-45 resize-y" className="font-mono text-xs min-h-45 resize-y"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Use template variables to reference {t("httpDestBodyTemplateHint")}
event fields in your payload.
</p> </p>
</div> </div>
)} )}
@@ -601,11 +565,10 @@ export function HttpDestinationCredenza({
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="font-medium block"> <label className="font-medium block">
Payload Format {t("httpDestPayloadFormatTitle")}
</label> </label>
<p className="text-sm text-muted-foreground mt-0.5"> <p className="text-sm text-muted-foreground mt-0.5">
How events are serialised into each {t("httpDestPayloadFormatDescription")}
request body.
</p> </p>
</div> </div>
@@ -630,16 +593,10 @@ export function HttpDestinationCredenza({
htmlFor="fmt-json-array" htmlFor="fmt-json-array"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
JSON Array {t("httpDestFormatJsonArrayTitle")}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
One request per batch, body is {t("httpDestFormatJsonArrayDescription")}
a JSON array{" "}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
[{"{...}"}, {"{...}"}]
</code>
. Compatible with most generic
webhooks and Datadog.
</p> </p>
</div> </div>
</div> </div>
@@ -656,19 +613,10 @@ export function HttpDestinationCredenza({
htmlFor="fmt-ndjson" htmlFor="fmt-ndjson"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
NDJSON {t("httpDestFormatNdjsonTitle")}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
One request per batch, body is {t("httpDestFormatNdjsonDescription")}
newline-delimited JSON one
object per line, no outer
array. Required by{" "}
<strong>Splunk HEC</strong>,{" "}
<strong>
Elastic / OpenSearch
</strong>
, and{" "}
<strong>Grafana Loki</strong>.
</p> </p>
</div> </div>
</div> </div>
@@ -685,13 +633,10 @@ export function HttpDestinationCredenza({
htmlFor="fmt-json-single" htmlFor="fmt-json-single"
className="cursor-pointer font-medium" className="cursor-pointer font-medium"
> >
One Event Per Request {t("httpDestFormatSingleTitle")}
</Label> </Label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Sends a separate HTTP POST for {t("httpDestFormatSingleDescription")}
each individual event. Use only
for endpoints that cannot
handle batches.
</p> </p>
</div> </div>
</div> </div>
@@ -703,12 +648,10 @@ export function HttpDestinationCredenza({
<div className="space-y-6 mt-4 p-1"> <div className="space-y-6 mt-4 p-1">
<div> <div>
<label className="font-medium block"> <label className="font-medium block">
Log Types {t("httpDestLogTypesTitle")}
</label> </label>
<p className="text-sm text-muted-foreground mt-0.5"> <p className="text-sm text-muted-foreground mt-0.5">
Choose which log types are forwarded to {t("httpDestLogTypesDescription")}
this destination. Only enabled log types
will be streamed.
</p> </p>
</div> </div>
@@ -727,11 +670,10 @@ export function HttpDestinationCredenza({
htmlFor="log-access" htmlFor="log-access"
className="text-sm font-medium cursor-pointer" className="text-sm font-medium cursor-pointer"
> >
Access Logs {t("httpDestAccessLogsTitle")}
</label> </label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Resource access attempts, including {t("httpDestAccessLogsDescription")}
authenticated and denied requests.
</p> </p>
</div> </div>
</div> </div>
@@ -750,11 +692,10 @@ export function HttpDestinationCredenza({
htmlFor="log-action" htmlFor="log-action"
className="text-sm font-medium cursor-pointer" className="text-sm font-medium cursor-pointer"
> >
Action Logs {t("httpDestActionLogsTitle")}
</label> </label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Administrative actions performed by {t("httpDestActionLogsDescription")}
users within the organization.
</p> </p>
</div> </div>
</div> </div>
@@ -773,12 +714,10 @@ export function HttpDestinationCredenza({
htmlFor="log-connection" htmlFor="log-connection"
className="text-sm font-medium cursor-pointer" className="text-sm font-medium cursor-pointer"
> >
Connection Logs {t("httpDestConnectionLogsTitle")}
</label> </label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Site and tunnel connection events, {t("httpDestConnectionLogsDescription")}
including connects and
disconnects.
</p> </p>
</div> </div>
</div> </div>
@@ -797,12 +736,10 @@ export function HttpDestinationCredenza({
htmlFor="log-request" htmlFor="log-request"
className="text-sm font-medium cursor-pointer" className="text-sm font-medium cursor-pointer"
> >
Request Logs {t("httpDestRequestLogsTitle")}
</label> </label>
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
HTTP request logs for proxied {t("httpDestRequestLogsDescription")}
resources, including method, path,
and response code.
</p> </p>
</div> </div>
</div> </div>
@@ -818,7 +755,7 @@ export function HttpDestinationCredenza({
variant="outline" variant="outline"
disabled={saving} disabled={saving}
> >
Cancel {t("cancel")}
</Button> </Button>
</CredenzaClose> </CredenzaClose>
<Button <Button
@@ -827,7 +764,7 @@ export function HttpDestinationCredenza({
loading={saving} loading={saving}
disabled={!isValid || saving} disabled={!isValid || saving}
> >
{editing ? "Save Changes" : "Create Destination"} {editing ? t("httpDestSaveChanges") : t("httpDestCreateDestination")}
</Button> </Button>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>