mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-18 06:51:44 +00:00
Add streaming errors for debug
This commit is contained in:
@@ -3062,7 +3062,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
||||||
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
||||||
"streamingFailedToLoad": "Failed to load destinations",
|
"streamingLastSyncError": "An error occurred on the last sync",
|
||||||
"streamingUnexpectedError": "An unexpected error occurred.",
|
"streamingUnexpectedError": "An unexpected error occurred.",
|
||||||
"streamingFailedToUpdate": "Failed to update destination",
|
"streamingFailedToUpdate": "Failed to update destination",
|
||||||
"streamingDeletedSuccess": "Destination deleted successfully",
|
"streamingDeletedSuccess": "Destination deleted successfully",
|
||||||
|
|||||||
@@ -439,6 +439,8 @@ export const eventStreamingDestinations = pgTable(
|
|||||||
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
||||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
lastError: text("lastError"), // last send error message, null if healthy
|
||||||
|
lastErrorAt: bigint("lastErrorAt", { mode: "number" }), // epoch ms of last error, null if healthy
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,6 +445,8 @@ export const eventStreamingDestinations = sqliteTable(
|
|||||||
enabled: integer("enabled", { mode: "boolean" })
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
lastError: text("lastError"), // last send error message, null if healthy
|
||||||
|
lastErrorAt: integer("lastErrorAt"), // epoch ms of last error, null if healthy
|
||||||
createdAt: integer("createdAt").notNull(),
|
createdAt: integer("createdAt").notNull(),
|
||||||
updatedAt: integer("updatedAt").notNull()
|
updatedAt: integer("updatedAt").notNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,6 +313,7 @@ export class LogStreamingManager {
|
|||||||
if (enabledTypes.length === 0) return;
|
if (enabledTypes.length === 0) return;
|
||||||
|
|
||||||
let anyFailure = false;
|
let anyFailure = false;
|
||||||
|
let firstError: string | null = null;
|
||||||
|
|
||||||
for (const logType of enabledTypes) {
|
for (const logType of enabledTypes) {
|
||||||
if (!this.isRunning) break;
|
if (!this.isRunning) break;
|
||||||
@@ -320,6 +321,10 @@ export class LogStreamingManager {
|
|||||||
await this.processLogType(dest, provider, logType);
|
await this.processLogType(dest, provider, logType);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
anyFailure = true;
|
anyFailure = true;
|
||||||
|
if (firstError === null) {
|
||||||
|
firstError =
|
||||||
|
err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
`LogStreamingManager: failed to process "${logType}" logs ` +
|
`LogStreamingManager: failed to process "${logType}" logs ` +
|
||||||
`for destination ${dest.destinationId}`,
|
`for destination ${dest.destinationId}`,
|
||||||
@@ -330,6 +335,10 @@ export class LogStreamingManager {
|
|||||||
|
|
||||||
if (anyFailure) {
|
if (anyFailure) {
|
||||||
this.recordFailure(dest.destinationId);
|
this.recordFailure(dest.destinationId);
|
||||||
|
await this.setDestinationError(
|
||||||
|
dest.destinationId,
|
||||||
|
firstError ?? "Unknown error"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Any success resets the failure/back-off state
|
// Any success resets the failure/back-off state
|
||||||
if (this.failures.has(dest.destinationId)) {
|
if (this.failures.has(dest.destinationId)) {
|
||||||
@@ -338,6 +347,7 @@ export class LogStreamingManager {
|
|||||||
`LogStreamingManager: destination ${dest.destinationId} recovered`
|
`LogStreamingManager: destination ${dest.destinationId} recovered`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await this.clearDestinationError(dest.destinationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +769,45 @@ export class LogStreamingManager {
|
|||||||
// DB helpers
|
// DB helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private async setDestinationError(
|
||||||
|
destinationId: number,
|
||||||
|
errorMessage: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Truncate to 1000 chars so it fits comfortably in the text column.
|
||||||
|
const truncated = errorMessage.slice(0, 1000);
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.update(eventStreamingDestinations)
|
||||||
|
.set({ lastError: truncated, lastErrorAt: Date.now() })
|
||||||
|
.where(
|
||||||
|
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`LogStreamingManager: could not persist error status for destination ${destinationId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearDestinationError(destinationId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Only update if there is actually an error stored, to avoid
|
||||||
|
// unnecessary writes on every successful poll cycle.
|
||||||
|
await db
|
||||||
|
.update(eventStreamingDestinations)
|
||||||
|
.set({ lastError: null, lastErrorAt: null })
|
||||||
|
.where(
|
||||||
|
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`LogStreamingManager: could not clear error status for destination ${destinationId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadEnabledDestinations(): Promise<
|
private async loadEnabledDestinations(): Promise<
|
||||||
EventStreamingDestination[]
|
EventStreamingDestination[]
|
||||||
> {
|
> {
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export type ListEventStreamingDestinationsResponse = {
|
|||||||
type: string;
|
type: string;
|
||||||
config: string;
|
config: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
lastError: string | null;
|
||||||
|
lastErrorAt: number | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
sendConnectionLogs: boolean;
|
sendConnectionLogs: boolean;
|
||||||
@@ -79,7 +81,8 @@ async function query(orgId: string, limit: number, offset: number) {
|
|||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/event-streaming-destination",
|
path: "/org/{orgId}/event-streaming-destination",
|
||||||
description: "List all event streaming destinations for a specific organization.",
|
description:
|
||||||
|
"List all event streaming destinations for a specific organization.",
|
||||||
tags: [OpenAPITags.Org],
|
tags: [OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
query: querySchema,
|
query: querySchema,
|
||||||
|
|||||||
@@ -22,7 +22,18 @@ import {
|
|||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { Globe, MoreHorizontal, Plus } from "lucide-react";
|
import {
|
||||||
|
Globe,
|
||||||
|
MoreHorizontal,
|
||||||
|
Plus,
|
||||||
|
AlertCircle,
|
||||||
|
ChevronDown
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@@ -153,6 +164,31 @@ function DestinationCard({
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Error indicator */}
|
||||||
|
{destination.lastError && (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1.5 text-left cursor-pointer rounded px-0 hover:opacity-75 transition-opacity"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-3.5 w-3.5 text-destructive shrink-0" />
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{t("streamingLastSyncError")}
|
||||||
|
</p>
|
||||||
|
<ChevronDown className="h-3 w-3 text-destructive shrink-0 ml-auto" />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
className="w-80 text-xs break-words"
|
||||||
|
>
|
||||||
|
{destination.lastError}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer: edit button + three-dots menu */}
|
{/* Footer: edit button + three-dots menu */}
|
||||||
<div className="mt-auto pt-5 flex gap-2">
|
<div className="mt-auto pt-5 flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
import { Textarea } from "@app/components/ui/textarea";
|
import { Textarea } from "@app/components/ui/textarea";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { Plus, X } from "lucide-react";
|
import { Plus, X, AlertCircle } from "lucide-react";
|
||||||
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
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";
|
||||||
@@ -56,6 +57,8 @@ export interface Destination {
|
|||||||
sendActionLogs: boolean;
|
sendActionLogs: boolean;
|
||||||
sendConnectionLogs: boolean;
|
sendConnectionLogs: boolean;
|
||||||
sendRequestLogs: boolean;
|
sendRequestLogs: boolean;
|
||||||
|
lastError: string | null;
|
||||||
|
lastErrorAt: number | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
@@ -122,9 +125,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={h.value}
|
value={h.value}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateRow(i, "value", e.target.value)}
|
||||||
updateRow(i, "value", e.target.value)
|
|
||||||
}
|
|
||||||
placeholder={t("httpDestHeaderValuePlaceholder")}
|
placeholder={t("httpDestHeaderValuePlaceholder")}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
@@ -200,10 +201,7 @@ export function HttpDestinationCredenza({
|
|||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(raw);
|
const parsed = new URL(raw);
|
||||||
if (
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||||
parsed.protocol !== "http:" &&
|
|
||||||
parsed.protocol !== "https:"
|
|
||||||
) {
|
|
||||||
return t("httpDestUrlErrorHttpRequired");
|
return t("httpDestUrlErrorHttpRequired");
|
||||||
}
|
}
|
||||||
if (build === "saas" && parsed.protocol !== "https:") {
|
if (build === "saas" && parsed.protocol !== "https:") {
|
||||||
@@ -216,9 +214,7 @@ export function HttpDestinationCredenza({
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
cfg.name.trim() !== "" &&
|
cfg.name.trim() !== "" && cfg.url.trim() !== "" && urlError === null;
|
||||||
cfg.url.trim() !== "" &&
|
|
||||||
urlError === null;
|
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!isValid) return;
|
if (!isValid) return;
|
||||||
@@ -253,10 +249,7 @@ export function HttpDestinationCredenza({
|
|||||||
title: editing
|
title: editing
|
||||||
? t("httpDestUpdateFailed")
|
? t("httpDestUpdateFailed")
|
||||||
: t("httpDestCreateFailed"),
|
: t("httpDestCreateFailed"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||||
e,
|
|
||||||
t("streamingUnexpectedError")
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
@@ -280,6 +273,14 @@ export function HttpDestinationCredenza({
|
|||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
|
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
|
{editing?.lastError && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription className="break-words">
|
||||||
|
{editing.lastError}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<HorizontalTabs
|
<HorizontalTabs
|
||||||
clientSide
|
clientSide
|
||||||
items={[
|
items={[
|
||||||
@@ -357,7 +358,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestAuthNoneTitle")}
|
{t("httpDestAuthNoneTitle")}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthNoneDescription")}
|
{t(
|
||||||
|
"httpDestAuthNoneDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,15 +378,21 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="auth-bearer"
|
htmlFor="auth-bearer"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestAuthBearerTitle")}
|
{t(
|
||||||
|
"httpDestAuthBearerTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthBearerDescription")}
|
{t(
|
||||||
|
"httpDestAuthBearerDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{cfg.authType === "bearer" && (
|
{cfg.authType === "bearer" && (
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthBearerPlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthBearerPlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.bearerToken ?? ""
|
cfg.bearerToken ?? ""
|
||||||
}
|
}
|
||||||
@@ -411,15 +420,21 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="auth-basic"
|
htmlFor="auth-basic"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestAuthBasicTitle")}
|
{t(
|
||||||
|
"httpDestAuthBasicTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthBasicDescription")}
|
{t(
|
||||||
|
"httpDestAuthBasicDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{cfg.authType === "basic" && (
|
{cfg.authType === "basic" && (
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthBasicPlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthBasicPlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.basicCredentials ??
|
cfg.basicCredentials ??
|
||||||
""
|
""
|
||||||
@@ -448,16 +463,22 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="auth-custom"
|
htmlFor="auth-custom"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestAuthCustomTitle")}
|
{t(
|
||||||
|
"httpDestAuthCustomTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestAuthCustomDescription")}
|
{t(
|
||||||
|
"httpDestAuthCustomDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{cfg.authType === "custom" && (
|
{cfg.authType === "custom" && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthCustomHeaderNamePlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthCustomHeaderNamePlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.customHeaderName ??
|
cfg.customHeaderName ??
|
||||||
""
|
""
|
||||||
@@ -472,7 +493,9 @@ export function HttpDestinationCredenza({
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("httpDestAuthCustomHeaderValuePlaceholder")}
|
placeholder={t(
|
||||||
|
"httpDestAuthCustomHeaderValuePlaceholder"
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
cfg.customHeaderValue ??
|
cfg.customHeaderValue ??
|
||||||
""
|
""
|
||||||
@@ -593,10 +616,14 @@ export function HttpDestinationCredenza({
|
|||||||
htmlFor="fmt-json-array"
|
htmlFor="fmt-json-array"
|
||||||
className="cursor-pointer font-medium"
|
className="cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{t("httpDestFormatJsonArrayTitle")}
|
{t(
|
||||||
|
"httpDestFormatJsonArrayTitle"
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestFormatJsonArrayDescription")}
|
{t(
|
||||||
|
"httpDestFormatJsonArrayDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -616,7 +643,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestFormatNdjsonTitle")}
|
{t("httpDestFormatNdjsonTitle")}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestFormatNdjsonDescription")}
|
{t(
|
||||||
|
"httpDestFormatNdjsonDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -636,7 +665,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestFormatSingleTitle")}
|
{t("httpDestFormatSingleTitle")}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestFormatSingleDescription")}
|
{t(
|
||||||
|
"httpDestFormatSingleDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -717,7 +748,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestConnectionLogsTitle")}
|
{t("httpDestConnectionLogsTitle")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestConnectionLogsDescription")}
|
{t(
|
||||||
|
"httpDestConnectionLogsDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -739,7 +772,9 @@ export function HttpDestinationCredenza({
|
|||||||
{t("httpDestRequestLogsTitle")}
|
{t("httpDestRequestLogsTitle")}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{t("httpDestRequestLogsDescription")}
|
{t(
|
||||||
|
"httpDestRequestLogsDescription"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -764,10 +799,12 @@ export function HttpDestinationCredenza({
|
|||||||
loading={saving}
|
loading={saving}
|
||||||
disabled={!isValid || saving}
|
disabled={!isValid || saving}
|
||||||
>
|
>
|
||||||
{editing ? t("httpDestSaveChanges") : t("httpDestCreateDestination")}
|
{editing
|
||||||
|
? t("httpDestSaveChanges")
|
||||||
|
: t("httpDestCreateDestination")}
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { Switch } from "@app/components/ui/switch";
|
|||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
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";
|
||||||
@@ -164,6 +166,14 @@ export function S3DestinationCredenza({
|
|||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
|
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
|
{editing?.lastError && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription className="break-words">
|
||||||
|
{editing.lastError}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<HorizontalTabs
|
<HorizontalTabs
|
||||||
clientSide
|
clientSide
|
||||||
items={[
|
items={[
|
||||||
|
|||||||
Reference in New Issue
Block a user