@@ -51,13 +46,13 @@ export function IntegrationCard({
{isConnected && (
)}
-
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/webapp/app/components/integrations/IntegrationGrid.tsx b/apps/webapp/app/components/integrations/integration-grid.tsx
similarity index 57%
rename from apps/webapp/app/components/integrations/IntegrationGrid.tsx
rename to apps/webapp/app/components/integrations/integration-grid.tsx
index 2330ad4..f196af9 100644
--- a/apps/webapp/app/components/integrations/IntegrationGrid.tsx
+++ b/apps/webapp/app/components/integrations/integration-grid.tsx
@@ -1,7 +1,6 @@
import React, { useMemo } from "react";
import { Search } from "lucide-react";
-import { IntegrationCard } from "./IntegrationCard";
-import { IntegrationAuthDialog } from "./IntegrationAuthDialog";
+import { IntegrationCard } from "./integration-card";
interface IntegrationGridProps {
integrations: Array<{
@@ -19,7 +18,6 @@ interface IntegrationGridProps {
export function IntegrationGrid({
integrations,
activeAccountIds,
- showDetail = false,
}: IntegrationGridProps) {
const hasActiveAccount = (integrationDefinitionId: string) =>
activeAccountIds.has(integrationDefinitionId);
@@ -34,33 +32,17 @@ export function IntegrationGrid({
}
return (
-
+
{integrations.map((integration) => {
const isConnected = hasActiveAccount(integration.id);
- if (showDetail) {
- return (
-
- );
- }
-
return (
-
-
-
+ isConnected={isConnected}
+ />
);
})}
);
-}
\ No newline at end of file
+}
diff --git a/apps/webapp/app/components/integrations/mcp-auth-section.tsx b/apps/webapp/app/components/integrations/mcp-auth-section.tsx
new file mode 100644
index 0000000..4c7105e
--- /dev/null
+++ b/apps/webapp/app/components/integrations/mcp-auth-section.tsx
@@ -0,0 +1,133 @@
+import React, { useCallback, useState } from "react";
+import { useFetcher } from "@remix-run/react";
+import { Button } from "~/components/ui/button";
+import { Check } from "lucide-react";
+
+interface MCPAuthSectionProps {
+ integration: {
+ id: string;
+ name: string;
+ };
+ activeAccount?: {
+ id: string;
+ integrationConfiguration?: {
+ mcp?: any;
+ };
+ };
+ hasMCPAuth: boolean;
+}
+
+export function MCPAuthSection({
+ integration,
+ activeAccount,
+ hasMCPAuth,
+}: MCPAuthSectionProps) {
+ const [isMCPConnecting, setIsMCPConnecting] = useState(false);
+ const mcpFetcher = useFetcher<{ redirectURL: string }>();
+ const disconnectMcpFetcher = useFetcher();
+
+ const isMCPConnected = activeAccount?.integrationConfiguration?.mcp;
+
+ const handleMCPConnect = useCallback(() => {
+ setIsMCPConnecting(true);
+ mcpFetcher.submit(
+ {
+ integrationDefinitionId: integration.id,
+ redirectURL: window.location.href,
+ integrationAccountId: activeAccount?.id as string,
+ mcp: true,
+ },
+ {
+ method: "post",
+ action: "/api/v1/oauth",
+ encType: "application/json",
+ },
+ );
+ }, [integration.id, mcpFetcher]);
+
+ const handleMCPDisconnect = useCallback(() => {
+ if (!activeAccount?.id) return;
+
+ disconnectMcpFetcher.submit(
+ {
+ integrationAccountId: activeAccount.id,
+ },
+ {
+ method: "post",
+ action: "/api/v1/integration_account/disconnect_mcp",
+ encType: "application/json",
+ },
+ );
+ }, [activeAccount?.id, disconnectMcpFetcher]);
+
+ // Watch for fetcher completion
+ React.useEffect(() => {
+ if (mcpFetcher.state === "idle" && isMCPConnecting) {
+ if (mcpFetcher.data?.redirectURL) {
+ window.location.href = mcpFetcher.data.redirectURL;
+ } else {
+ setIsMCPConnecting(false);
+ }
+ }
+ }, [mcpFetcher.state, mcpFetcher.data, isMCPConnecting]);
+
+ React.useEffect(() => {
+ if (disconnectMcpFetcher.state === "idle" && disconnectMcpFetcher.data) {
+ window.location.reload();
+ }
+ }, [disconnectMcpFetcher.state, disconnectMcpFetcher.data]);
+
+ if (!hasMCPAuth || !activeAccount) return null;
+
+ return (
+
+
MCP Authentication
+
+ {isMCPConnected ? (
+
+
+
+ MCP Connected
+
+
+ MCP (Model Context Protocol) authentication is active
+
+
+
+
+
+
+ ) : activeAccount ? (
+
+
+ MCP (Model Context Protocol) Authentication
+
+
+ This integration requires MCP (Model Context Protocol)
+ authentication. Please provide the required MCP credentials in
+ addition to any other authentication method.
+
+
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/apps/webapp/app/components/integrations/oauth-auth-section.tsx b/apps/webapp/app/components/integrations/oauth-auth-section.tsx
new file mode 100644
index 0000000..cdbe0eb
--- /dev/null
+++ b/apps/webapp/app/components/integrations/oauth-auth-section.tsx
@@ -0,0 +1,68 @@
+import React, { useState, useCallback } from "react";
+import { useFetcher } from "@remix-run/react";
+import { Button } from "~/components/ui/button";
+
+interface OAuthAuthSectionProps {
+ integration: {
+ id: string;
+ name: string;
+ };
+ specData: any;
+ activeAccount: any;
+}
+
+export function OAuthAuthSection({
+ integration,
+ specData,
+ activeAccount,
+}: OAuthAuthSectionProps) {
+ const [isConnecting, setIsConnecting] = useState(false);
+ const oauthFetcher = useFetcher<{ redirectURL: string }>();
+
+ const handleOAuthConnect = useCallback(() => {
+ setIsConnecting(true);
+ oauthFetcher.submit(
+ {
+ integrationDefinitionId: integration.id,
+ redirectURL: window.location.href,
+ },
+ {
+ method: "post",
+ action: "/api/v1/oauth",
+ encType: "application/json",
+ },
+ );
+ }, [integration.id, oauthFetcher]);
+
+ React.useEffect(() => {
+ if (oauthFetcher.state === "idle" && isConnecting) {
+ if (oauthFetcher.data?.redirectURL) {
+ window.location.href = oauthFetcher.data.redirectURL;
+ } else {
+ setIsConnecting(false);
+ }
+ }
+ }, [oauthFetcher.state, oauthFetcher.data, isConnecting]);
+
+ if (activeAccount || !specData?.auth?.OAuth2) {
+ return null;
+ }
+
+ return (
+
+
OAuth 2.0 Authentication
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/webapp/app/components/integrations/section.tsx b/apps/webapp/app/components/integrations/section.tsx
new file mode 100644
index 0000000..d0c63fa
--- /dev/null
+++ b/apps/webapp/app/components/integrations/section.tsx
@@ -0,0 +1,33 @@
+interface SectionProps {
+ icon?: React.ReactNode;
+ title: string;
+ description: string;
+ metadata?: React.ReactNode;
+ children: React.ReactNode;
+}
+
+export function Section({
+ icon,
+ title,
+ description,
+ metadata,
+ children,
+}: SectionProps) {
+ return (
+
+
+ {icon && <>{icon}>}
+
{title}
+
{description}
+ {metadata ? metadata : null}
+
+
+
+ );
+}
diff --git a/apps/webapp/app/components/logs/virtual-logs-list.tsx b/apps/webapp/app/components/logs/virtual-logs-list.tsx
index 54d9fd4..ab3bf42 100644
--- a/apps/webapp/app/components/logs/virtual-logs-list.tsx
+++ b/apps/webapp/app/components/logs/virtual-logs-list.tsx
@@ -1,8 +1,6 @@
import { useEffect, useRef, useState } from "react";
import {
- List,
InfiniteLoader,
- WindowScroller,
AutoSizer,
CellMeasurer,
CellMeasurerCache,
@@ -12,10 +10,97 @@ import {
import { type LogItem } from "~/hooks/use-logs";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent } from "~/components/ui/card";
-import { AlertCircle, CheckCircle, Clock, XCircle } from "lucide-react";
+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 && (
+
+ )}
+
+ >
+ );
+}
+
interface VirtualLogsListProps {
logs: LogItem[];
hasMore: boolean;
@@ -48,37 +133,20 @@ function LogItemRenderer(
);
}
- const getStatusIcon = (status: string) => {
- switch (status) {
- case "PROCESSING":
- return
;
- case "PENDING":
- return
;
- case "COMPLETED":
- return
;
- case "FAILED":
- return
;
- case "CANCELLED":
- return
;
- default:
- return
;
- }
- };
-
const getStatusColor = (status: string) => {
switch (status) {
case "PROCESSING":
- return "bg-blue-100 text-blue-800";
+ return "bg-blue-100 text-blue-800 hover:bg-blue-100 hover:text-blue-800";
case "PENDING":
- return "bg-yellow-100 text-yellow-800";
+ return "bg-yellow-100 text-yellow-800 hover:bg-yellow-100 hover:text-yellow-800";
case "COMPLETED":
- return "bg-green-100 text-green-800";
+ return "bg-green-100 text-green-800 hover:bg-green-100 hover:text-green-800";
case "FAILED":
- return "bg-red-100 text-red-800";
+ return "bg-red-100 text-red-800 hover:bg-red-100 hover:text-red-800";
case "CANCELLED":
- return "bg-gray-100 text-gray-800";
+ return "bg-gray-100 text-gray-800 hover:bg-gray-100 hover:text-gray-800";
default:
- return "bg-gray-100 text-gray-800";
+ return "bg-gray-100 text-gray-800 hover:bg-gray-100 hover:text-gray-800";
}
};
@@ -95,13 +163,18 @@ function LogItemRenderer(
-
+
{log.source}
- {getStatusIcon(log.status)}
-
- {log.status.toLowerCase()}
+
+ {log.status.charAt(0).toUpperCase() +
+ log.status.slice(1).toLowerCase()}
@@ -110,38 +183,7 @@ function LogItemRenderer(