diff --git a/apps/webapp/app/components/logs/log-text-collapse.tsx b/apps/webapp/app/components/logs/log-text-collapse.tsx
new file mode 100644
index 0000000..f5bdff4
--- /dev/null
+++ b/apps/webapp/app/components/logs/log-text-collapse.tsx
@@ -0,0 +1,166 @@
+import { useState } from "react";
+import { useFetcher } from "@remix-run/react";
+import { AlertCircle, Info, Trash } from "lucide-react";
+import { cn } from "~/lib/utils";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
+import { Button } from "../ui";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "../ui/alert-dialog";
+
+interface LogTextCollapseProps {
+ text?: string;
+ error?: string;
+ logData: any;
+ episodeUUID?: string;
+}
+
+export function LogTextCollapse({
+ episodeUUID,
+ text,
+ error,
+ logData,
+}: LogTextCollapseProps) {
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const deleteFetcher = useFetcher();
+
+ const handleDelete = () => {
+ console.log(logData);
+ if (!episodeUUID) {
+ console.error("No episodeUuid found in log data");
+ return;
+ }
+
+ deleteFetcher.submit(
+ { episodeUuid: episodeUUID },
+ {
+ method: "DELETE",
+ action: "/api/v1/episode/delete",
+ encType: "application/json",
+ },
+ );
+ setDeleteDialogOpen(false);
+ };
+
+ // Show collapse if text is long (by word count)
+ const COLLAPSE_WORD_LIMIT = 30;
+
+ if (!text) {
+ return (
+
+ No log details.
+
+ );
+ }
+
+ // Split by words for word count
+ const words = text.split(/\s+/);
+ const isLong = words.length > COLLAPSE_WORD_LIMIT;
+
+ let displayText: string;
+ if (isLong) {
+ displayText = words.slice(0, COLLAPSE_WORD_LIMIT).join(" ") + " ...";
+ } else {
+ displayText = text;
+ }
+
+ return (
+ <>
+
+
+ {displayText}
+
+ {isLong && (
+ <>
+
+ >
+ )}
+
+
+ {isLong && (
+
+
+ {episodeUUID && (
+
+
+
+
+
+
+ Delete Episode
+
+ Are you sure you want to delete this episode? This action
+ cannot be undone.
+
+
+
+ Cancel
+
+ Continue
+
+
+
+
+ )}
+
+ )}
+ {error && (
+
+ )}
+
+ >
+ );
+}
diff --git a/apps/webapp/app/components/logs/virtual-logs-list.tsx b/apps/webapp/app/components/logs/virtual-logs-list.tsx
index ab3bf42..be9484a 100644
--- a/apps/webapp/app/components/logs/virtual-logs-list.tsx
+++ b/apps/webapp/app/components/logs/virtual-logs-list.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef } from "react";
import {
InfiniteLoader,
AutoSizer,
@@ -10,96 +10,9 @@ import {
import { type LogItem } from "~/hooks/use-logs";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent } from "~/components/ui/card";
-import { AlertCircle } from "lucide-react";
import { cn } from "~/lib/utils";
import { ScrollManagedList } from "../virtualized-list";
-
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
-import { Button } from "../ui";
-
-// --- LogTextCollapse component ---
-function LogTextCollapse({ text, error }: { text?: string; error?: string }) {
- const [dialogOpen, setDialogOpen] = useState(false);
-
- // Show collapse if text is long (by word count)
- const COLLAPSE_WORD_LIMIT = 30;
-
- if (!text) {
- return (
-
- No log details.
-
- );
- }
-
- // Split by words for word count
- const words = text.split(/\s+/);
- const isLong = words.length > COLLAPSE_WORD_LIMIT;
-
- let displayText: string;
- if (isLong) {
- displayText = words.slice(0, COLLAPSE_WORD_LIMIT).join(" ") + " ...";
- } else {
- displayText = text;
- }
-
- return (
- <>
-
-
- {displayText}
-
- {isLong && (
- <>
-
- >
- )}
-
-
- {isLong && (
-
- )}
- {error && (
-
- )}
-
- >
- );
-}
+import { LogTextCollapse } from "./log-text-collapse";
interface VirtualLogsListProps {
logs: LogItem[];
@@ -183,7 +96,12 @@ function LogItemRenderer(
-
+
@@ -196,7 +114,6 @@ export function VirtualLogsList({
hasMore,
loadMore,
isLoading,
- height = 600,
}: VirtualLogsListProps) {
// Create a CellMeasurerCache instance using useRef to prevent recreation
const cacheRef = useRef(null);
diff --git a/apps/webapp/app/components/ui/alert-dialog.tsx b/apps/webapp/app/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..7a6229c
--- /dev/null
+++ b/apps/webapp/app/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import React from "react";
+
+import { buttonVariants } from "./button";
+import { cn } from "../../lib/utils";
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = "AlertDialogHeader";
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = "AlertDialogFooter";
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/apps/webapp/app/hooks/use-logs.tsx b/apps/webapp/app/hooks/use-logs.tsx
index 5156d22..16d6749 100644
--- a/apps/webapp/app/hooks/use-logs.tsx
+++ b/apps/webapp/app/hooks/use-logs.tsx
@@ -12,6 +12,8 @@ export interface LogItem {
sourceURL?: string;
integrationSlug?: string;
activityId?: string;
+ episodeUUID?: string;
+ data?: any;
}
export interface LogsResponse {
diff --git a/apps/webapp/app/routes/api.v1.episode.delete.tsx b/apps/webapp/app/routes/api.v1.episode.delete.tsx
index 55dff0c..94c5ef0 100644
--- a/apps/webapp/app/routes/api.v1.episode.delete.tsx
+++ b/apps/webapp/app/routes/api.v1.episode.delete.tsx
@@ -1,13 +1,13 @@
import { z } from "zod";
import { json } from "@remix-run/node";
-import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
+import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { deleteEpisodeWithRelatedNodes } from "~/services/graphModels/episode";
export const DeleteEpisodeBodyRequest = z.object({
episodeUuid: z.string().uuid("Episode UUID must be a valid UUID"),
});
-const { action, loader } = createActionApiRoute(
+const { action, loader } = createHybridActionApiRoute(
{
body: DeleteEpisodeBodyRequest,
allowJWT: true,
diff --git a/apps/webapp/app/routes/api.v1.logs.activity.tsx b/apps/webapp/app/routes/api.v1.logs.activity.tsx
index 712c519..fd8301f 100644
--- a/apps/webapp/app/routes/api.v1.logs.activity.tsx
+++ b/apps/webapp/app/routes/api.v1.logs.activity.tsx
@@ -116,10 +116,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
processedAt: log.processedAt,
status: log.status,
error: log.error,
+ episodeUUID: (log.output as any)?.episodeUuid,
sourceURL: log.activity?.sourceURL,
integrationSlug:
log.activity?.integrationAccount?.integrationDefinition?.slug,
activityId: log.activityId,
+ data: log.data,
}));
return json({
diff --git a/apps/webapp/app/routes/api.v1.logs.all.tsx b/apps/webapp/app/routes/api.v1.logs.all.tsx
index f58214b..35a1da2 100644
--- a/apps/webapp/app/routes/api.v1.logs.all.tsx
+++ b/apps/webapp/app/routes/api.v1.logs.all.tsx
@@ -60,6 +60,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
processedAt: true,
status: true,
error: true,
+ output: true,
data: true,
activity: {
select: {
@@ -120,10 +121,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
"No content",
time: log.createdAt,
processedAt: log.processedAt,
+ episodeUUID: (log.output as any)?.episodeUuid,
status: log.status,
error: log.error,
sourceURL: log.activity?.sourceURL,
integrationSlug: integrationDef?.slug,
+ data: log.data,
};
});