diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index d7bc60f3e..bd41ab776 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -156,7 +156,7 @@ jobs:
timeout: 5m0s
name: ${{ env.SERVICE_NAME }}
chart-repository: https://techops-services.github.io/helm-charts
- version: 0.0.33
+ version: 0.2.1
atomic: true
- name: Deploying Job Spawner to Kubernetes with Helm
@@ -171,5 +171,5 @@ jobs:
timeout: 5m0s
name: keeperhub-job-spawner
chart-repository: https://techops-services.github.io/helm-charts
- version: 0.0.33
+ version: 0.2.1
atomic: true
diff --git a/Dockerfile b/Dockerfile
index f2dead431..9b08d60be 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -39,6 +39,7 @@ COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/drizzle ./drizzle
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
COPY --from=builder /app/lib ./lib
+COPY --from=builder /app/keeperhub ./keeperhub
COPY --from=builder /app/package.json ./package.json
# This stage can be used to run migrations
diff --git a/app/api/workflows/public/route.ts b/app/api/workflows/public/route.ts
new file mode 100644
index 000000000..17f7f6b4b
--- /dev/null
+++ b/app/api/workflows/public/route.ts
@@ -0,0 +1,35 @@
+import { desc, eq } from "drizzle-orm";
+import { NextResponse } from "next/server";
+import { db } from "@/lib/db";
+import { workflows } from "@/lib/db/schema";
+
+// start custom KeeperHub code
+export async function GET() {
+ try {
+ const publicWorkflows = await db
+ .select()
+ .from(workflows)
+ .where(eq(workflows.visibility, "public"))
+ .orderBy(desc(workflows.updatedAt));
+
+ const mappedWorkflows = publicWorkflows.map((workflow) => ({
+ ...workflow,
+ createdAt: workflow.createdAt.toISOString(),
+ updatedAt: workflow.updatedAt.toISOString(),
+ }));
+
+ return NextResponse.json(mappedWorkflows);
+ } catch (error) {
+ console.error("Failed to get public workflows:", error);
+ return NextResponse.json(
+ {
+ error:
+ error instanceof Error
+ ? error.message
+ : "Failed to get public workflows",
+ },
+ { status: 500 }
+ );
+ }
+}
+// end custom KeeperHub code
diff --git a/app/hub/page.tsx b/app/hub/page.tsx
new file mode 100644
index 000000000..1f7bfc130
--- /dev/null
+++ b/app/hub/page.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { WorkflowTemplateGrid } from "@/keeperhub/components/hub/workflow-template-grid";
+import { api, type SavedWorkflow } from "@/lib/api-client";
+
+export default function HubPage() {
+ // start custom KeeperHub code
+ const [workflows, setWorkflows] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchPublicWorkflows = async () => {
+ try {
+ const publicWorkflows = await api.workflow.getPublic();
+ setWorkflows(publicWorkflows);
+ } catch (error) {
+ console.error("Failed to fetch public workflows:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchPublicWorkflows();
+ }, []);
+ // end custom KeeperHub code
+
+ return (
+
+
+
Public Workflows
+ {/* start custom KeeperHub code */}
+ {isLoading ? (
+
Loading workflows...
+ ) : (
+
+ )}
+ {/* end custom KeeperHub code */}
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index f78df8c77..369809a39 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,17 +2,16 @@ import type { Metadata, Viewport } from "next";
import "./globals.css";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
-import { ReactFlowProvider } from "@xyflow/react";
import { Provider } from "jotai";
import { type ReactNode, Suspense } from "react";
import { AuthProvider } from "@/components/auth/provider";
import { GitHubStarsLoader } from "@/components/github-stars-loader";
import { GitHubStarsProvider } from "@/components/github-stars-provider";
import { GlobalModals } from "@/components/global-modals";
+import { LayoutContent } from "@/components/layout-content";
import { OverlayProvider } from "@/components/overlays/overlay-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
-import { PersistentCanvas } from "@/components/workflow/persistent-canvas";
import { KeeperHubExtensionLoader } from "@/keeperhub/components/extension-loader";
import { mono, sans } from "@/lib/fonts";
import { cn } from "@/lib/utils";
@@ -35,16 +34,6 @@ type RootLayoutProps = {
children: ReactNode;
};
-// Inner content wrapped by GitHubStarsProvider (used for both loading and loaded states)
-function LayoutContent({ children }: { children: ReactNode }) {
- return (
-
-
- {children}
-
- );
-}
-
const RootLayout = ({ children }: RootLayoutProps) => (
diff --git a/components/layout-content.tsx b/components/layout-content.tsx
new file mode 100644
index 000000000..f5cb7a01d
--- /dev/null
+++ b/components/layout-content.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { ReactFlowProvider } from "@xyflow/react";
+import type { ReactNode } from "react";
+import { PersistentCanvas } from "@/components/workflow/persistent-canvas";
+import { WorkflowToolbar } from "@/components/workflow/workflow-toolbar";
+
+export function LayoutContent({ children }: { children: ReactNode }) {
+ return (
+
+
+
+ {children}
+
+ );
+}
diff --git a/components/overlays/add-connection-overlay.tsx b/components/overlays/add-connection-overlay.tsx
index 999cc2db0..2449d7f55 100644
--- a/components/overlays/add-connection-overlay.tsx
+++ b/components/overlays/add-connection-overlay.tsx
@@ -14,6 +14,9 @@ import {
aiGatewayTeamsLoadingAtom,
} from "@/lib/ai-gateway/state";
import { api } from "@/lib/api-client";
+// start keeperhub
+import { getCustomIntegrationFormHandler } from "@/lib/extension-registry";
+// end keeperhub
import type { IntegrationType } from "@/lib/types/integration";
import {
getIntegration,
@@ -361,6 +364,18 @@ export function ConfigureConnectionOverlay({
// Render config fields
const renderConfigFields = () => {
+ // start keeperhub - check for custom form handlers (e.g., web3 wallet)
+ const customHandler = getCustomIntegrationFormHandler(type);
+ if (customHandler) {
+ return customHandler({
+ integrationType: type,
+ isEditMode: false,
+ config,
+ updateConfig,
+ });
+ }
+ // end keeperhub
+
if (type === "database") {
return (
{
if (!(selectedNode && isOwner)) {
return;
@@ -465,17 +467,26 @@ export function ConfigurationOverlay({ overlayId }: ConfigurationOverlayProps) {
)}
- {isOwner && (
+ {isOwner ? (
-
+ ) : (
+
+ DEBUG: isOwner is false - Delete button not rendered
+
)}
)}
diff --git a/components/overlays/edit-connection-overlay.tsx b/components/overlays/edit-connection-overlay.tsx
index 10e7b7f6f..3d43c4b72 100644
--- a/components/overlays/edit-connection-overlay.tsx
+++ b/components/overlays/edit-connection-overlay.tsx
@@ -9,7 +9,10 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useIsMobile } from "@/hooks/use-mobile";
import { api, type Integration } from "@/lib/api-client";
+// start keeperhub
+import { getCustomIntegrationFormHandler } from "@/lib/extension-registry";
import { getIntegration, getIntegrationLabels } from "@/plugins";
+// end keeperhub
import { ConfirmOverlay } from "./confirm-overlay";
import { Overlay } from "./overlay";
import { useOverlay } from "./overlay-provider";
@@ -267,6 +270,18 @@ export function EditConnectionOverlay({
// Render config fields
const renderConfigFields = () => {
+ // start keeperhub - check for custom form handlers (e.g., web3 wallet display)
+ const customHandler = getCustomIntegrationFormHandler(integration.type);
+ if (customHandler) {
+ return customHandler({
+ integrationType: integration.type,
+ isEditMode: true,
+ config,
+ updateConfig,
+ });
+ }
+ // end keeperhub
+
if (integration.type === "database") {
return (
- {hasExistingConnections && (
-
-
-
- )}
+ {/* start keeperhub - hide + button for singleConnection integrations */}
+ {hasExistingConnections &&
+ !getIntegration(integrationType)?.singleConnection && (
+
+
+
+ )}
+ {/* end keeperhub */}
{
const deleteNode = useSetAtom(deleteNodeAtom);
const deleteEdge = useSetAtom(deleteEdgeAtom);
const deleteSelectedItems = useSetAtom(deleteSelectedItemsAtom);
- const setShowClearDialog = useSetAtom(showClearDialogAtom);
- const setShowDeleteDialog = useSetAtom(showDeleteDialogAtom);
+ const [showClearDialog, setShowClearDialog] = useAtom(showClearDialogAtom);
+ const [showDeleteDialog, setShowDeleteDialog] = useAtom(showDeleteDialogAtom);
const clearNodeStatuses = useSetAtom(clearNodeStatusesAtom);
+ const clearWorkflow = useSetAtom(clearWorkflowAtom);
+ const { open: openOverlay } = useOverlay();
+
+ // Watch showDeleteDialog atom and open overlay when it becomes true
+ useEffect(() => {
+ if (showDeleteDialog) {
+ openOverlay(ConfirmOverlay, {
+ title: "Delete Workflow",
+ message: `Are you sure you want to delete "${currentWorkflowName}"? This will permanently delete the workflow. This cannot be undone.`,
+ confirmLabel: "Delete Workflow",
+ confirmVariant: "destructive" as const,
+ destructive: true,
+ onConfirm: async () => {
+ if (!currentWorkflowId) {
+ return;
+ }
+ try {
+ await api.workflow.delete(currentWorkflowId);
+ toast.success("Workflow deleted successfully");
+ window.location.href = "/";
+ } catch (_error) {
+ toast.error("Failed to delete workflow. Please try again.");
+ }
+ },
+ });
+ setShowDeleteDialog(false);
+ }
+ }, [
+ showDeleteDialog,
+ currentWorkflowId,
+ currentWorkflowName,
+ openOverlay,
+ setShowDeleteDialog,
+ ]);
+
+ // Watch showClearDialog atom and open overlay when it becomes true
+ useEffect(() => {
+ if (showClearDialog) {
+ openOverlay(ConfirmOverlay, {
+ title: "Clear Workflow",
+ message:
+ "Are you sure you want to clear all nodes and connections? This action cannot be undone.",
+ confirmLabel: "Clear Workflow",
+ confirmVariant: "destructive" as const,
+ destructive: true,
+ onConfirm: () => {
+ clearWorkflow();
+ },
+ });
+ setShowClearDialog(false);
+ }
+ }, [showClearDialog, openOverlay, clearWorkflow, setShowClearDialog]);
const setPendingIntegrationNodes = useSetAtom(pendingIntegrationNodesAtom);
const [newlyCreatedNodeId, setNewlyCreatedNodeId] = useAtom(
newlyCreatedNodeIdAtom
@@ -632,7 +685,9 @@ export const PanelInner = () => {
setShowDeleteDialog(true)}
+ onClick={() => {
+ setShowDeleteDialog(true);
+ }}
size="sm"
variant="ghost"
>
diff --git a/components/workflow/workflow-canvas.tsx b/components/workflow/workflow-canvas.tsx
index d7bf11af1..6f1285c5a 100644
--- a/components/workflow/workflow-canvas.tsx
+++ b/components/workflow/workflow-canvas.tsx
@@ -17,7 +17,6 @@ import { Canvas } from "@/components/ai-elements/canvas";
import { Connection } from "@/components/ai-elements/connection";
import { Controls } from "@/components/ai-elements/controls";
import { AIPrompt } from "@/components/ai-elements/prompt";
-import { WorkflowToolbar } from "@/components/workflow/workflow-toolbar";
import "@xyflow/react/dist/style.css";
import { PlayCircle, Zap } from "lucide-react";
@@ -77,7 +76,6 @@ const edgeTypes = {
temporary: Edge.Temporary,
};
-// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: React Flow canvas requires complex setup
export function WorkflowCanvas() {
const [nodes, setNodes] = useAtom(nodesAtom);
const [edges, setEdges] = useAtom(edgesAtom);
@@ -458,11 +456,6 @@ export function WorkflowCanvas() {
: "opacity 300ms",
}}
>
- {/* Toolbar */}
-
-
-
-
{/* React Flow Canvas */}