khotan-data 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +54 -0
- package/README.md +62 -0
- package/dist/cli.js +2585 -0
- package/dist/factory.cjs +2319 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.cts +475 -0
- package/dist/factory.d.ts +475 -0
- package/dist/factory.js +2311 -0
- package/dist/factory.js.map +1 -0
- package/dist/plug-client.cjs +99 -0
- package/dist/plug-client.cjs.map +1 -0
- package/dist/plug-client.d.cts +71 -0
- package/dist/plug-client.d.ts +71 -0
- package/dist/plug-client.js +96 -0
- package/dist/plug-client.js.map +1 -0
- package/dist/templates/agent-skill.md +73 -0
- package/dist/templates/agents.md +41 -0
- package/dist/templates/catch.example.ts +36 -0
- package/dist/templates/catch.ts +107 -0
- package/dist/templates/config-page.tsx +20 -0
- package/dist/templates/debug-index-page.tsx +101 -0
- package/dist/templates/debug-page.tsx +48 -0
- package/dist/templates/graph-page.tsx +11 -0
- package/dist/templates/hub.tsx +450 -0
- package/dist/templates/inflow.example.ts +61 -0
- package/dist/templates/inflow.ts +99 -0
- package/dist/templates/khotan-config.ts +40 -0
- package/dist/templates/khotan-route.ts +13 -0
- package/dist/templates/logs-page.tsx +9 -0
- package/dist/templates/logs.tsx +20 -0
- package/dist/templates/outflow.example.ts +52 -0
- package/dist/templates/outflow.ts +90 -0
- package/dist/templates/pass.example.ts +51 -0
- package/dist/templates/pass.ts +124 -0
- package/dist/templates/plug-debugger.tsx +1185 -0
- package/dist/templates/plug.example.ts +93 -0
- package/dist/templates/plug.ts +806 -0
- package/dist/templates/relay.example.ts +61 -0
- package/dist/templates/relay.ts +95 -0
- package/dist/templates/runs-table.tsx +592 -0
- package/dist/templates/schema.ts +424 -0
- package/dist/templates/skill-dashboard.md +144 -0
- package/dist/templates/skill-plug.md +193 -0
- package/dist/templates/skill-setup.md +119 -0
- package/dist/templates/skill-webhook.md +196 -0
- package/dist/templates/topology-canvas.tsx +1406 -0
- package/dist/templates/var-panel.tsx +276 -0
- package/dist/templates/webhook-events-table.tsx +241 -0
- package/dist/templates/wire-panel.tsx +216 -0
- package/dist/templates/wire.ts +155 -0
- package/package.json +46 -5
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { KhotanHub } from "@/components/khotan/hub";
|
|
2
|
+
|
|
3
|
+
function getWebhookUrl(): string {
|
|
4
|
+
return (
|
|
5
|
+
process.env.KHOTAN_WEBHOOK_URL ||
|
|
6
|
+
process.env.NGROK_URL ||
|
|
7
|
+
process.env.NEXT_PUBLIC_APP_URL ||
|
|
8
|
+
"http://localhost:3000"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function KhotanConfigPage() {
|
|
13
|
+
const webhookUrl = getWebhookUrl();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<main className="container mx-auto max-w-5xl px-4 py-10">
|
|
17
|
+
<KhotanHub webhookUrl={webhookUrl} />
|
|
18
|
+
</main>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Debug Index — Lists all registered plugs for debugging
|
|
8
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
9
|
+
//
|
|
10
|
+
// Place at: app/debug/page.tsx
|
|
11
|
+
// Requires KHOTAN_DEBUG env var to be set for the debug route to respond.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
interface Plug {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
authType: string;
|
|
19
|
+
status: "connected" | "error" | "idle";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function DebugIndexPage() {
|
|
23
|
+
const [plugs, setPlugs] = useState<Plug[]>([]);
|
|
24
|
+
const [loading, setLoading] = useState(true);
|
|
25
|
+
const [debugEnabled, setDebugEnabled] = useState<boolean | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
Promise.all([
|
|
29
|
+
fetch("/api/khotan/debug").then((r) => r.ok),
|
|
30
|
+
fetch("/api/khotan/plugs").then((r) => (r.ok ? r.json() : [])),
|
|
31
|
+
])
|
|
32
|
+
.then(([enabled, data]) => {
|
|
33
|
+
setDebugEnabled(enabled);
|
|
34
|
+
setPlugs(data as Plug[]);
|
|
35
|
+
})
|
|
36
|
+
.catch(() => setDebugEnabled(false))
|
|
37
|
+
.finally(() => setLoading(false));
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
if (loading) {
|
|
41
|
+
return (
|
|
42
|
+
<main className="container mx-auto max-w-3xl px-4 py-10">
|
|
43
|
+
<div className="h-8 w-48 animate-pulse rounded bg-muted" />
|
|
44
|
+
</main>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!debugEnabled) {
|
|
49
|
+
return (
|
|
50
|
+
<main className="container mx-auto max-w-3xl px-4 py-10">
|
|
51
|
+
<h1 className="text-2xl font-bold tracking-tight mb-4">Debug</h1>
|
|
52
|
+
<p className="text-muted-foreground">
|
|
53
|
+
Debug mode is not enabled. Set{" "}
|
|
54
|
+
<code className="bg-muted px-1.5 py-0.5 rounded text-sm">
|
|
55
|
+
KHOTAN_DEBUG=1
|
|
56
|
+
</code>{" "}
|
|
57
|
+
in your environment and restart the server.
|
|
58
|
+
</p>
|
|
59
|
+
</main>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<main className="container mx-auto max-w-3xl px-4 py-10">
|
|
65
|
+
<div className="mb-6">
|
|
66
|
+
<a
|
|
67
|
+
href="/config"
|
|
68
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
69
|
+
>
|
|
70
|
+
← Back to Hub
|
|
71
|
+
</a>
|
|
72
|
+
</div>
|
|
73
|
+
<h1 className="text-2xl font-bold tracking-tight mb-6">Debug</h1>
|
|
74
|
+
<p className="text-muted-foreground mb-6">
|
|
75
|
+
Select a plug to test requests through its real code path.
|
|
76
|
+
</p>
|
|
77
|
+
<div className="grid gap-3">
|
|
78
|
+
{plugs.map((plug) => (
|
|
79
|
+
<Link
|
|
80
|
+
key={plug.id}
|
|
81
|
+
href={`/debug/${plug.name}`}
|
|
82
|
+
className="flex items-center justify-between rounded-lg border border-border p-4 transition-colors hover:border-foreground/30 hover:bg-muted/50"
|
|
83
|
+
>
|
|
84
|
+
<div>
|
|
85
|
+
<p className="font-medium">{plug.name}</p>
|
|
86
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
87
|
+
{plug.baseUrl}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-2">
|
|
91
|
+
<span className="text-xs text-muted-foreground">
|
|
92
|
+
{plug.authType}
|
|
93
|
+
</span>
|
|
94
|
+
<span className="text-muted-foreground">→</span>
|
|
95
|
+
</div>
|
|
96
|
+
</Link>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</main>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useParams } from "next/navigation";
|
|
4
|
+
import { PlugDebugger } from "@/components/khotan/plug-debugger";
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Debug Page — Route for plug debugging
|
|
8
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
9
|
+
//
|
|
10
|
+
// Place at: app/debug/[plugName]/page.tsx
|
|
11
|
+
// Requires KHOTAN_DEBUG env var to be set for the debug route to respond.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export default function PlugDebugPage() {
|
|
15
|
+
const params = useParams<{ plugName: string }>();
|
|
16
|
+
const plugName = params.plugName;
|
|
17
|
+
|
|
18
|
+
if (!plugName) {
|
|
19
|
+
return (
|
|
20
|
+
<main className="container mx-auto max-w-6xl px-4 py-10">
|
|
21
|
+
<p className="text-muted-foreground">No plug specified.</p>
|
|
22
|
+
</main>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<main className="container mx-auto max-w-6xl px-4 py-10">
|
|
28
|
+
<div className="flex items-center justify-between mb-6">
|
|
29
|
+
<a
|
|
30
|
+
href="/debug"
|
|
31
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
32
|
+
>
|
|
33
|
+
← All Plugs
|
|
34
|
+
</a>
|
|
35
|
+
<a
|
|
36
|
+
href="/config"
|
|
37
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
38
|
+
>
|
|
39
|
+
Hub
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
<h1 className="text-2xl font-bold tracking-tight mb-6">
|
|
43
|
+
Debug: {plugName}
|
|
44
|
+
</h1>
|
|
45
|
+
<PlugDebugger plugName={plugName} />
|
|
46
|
+
</main>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { KhotanTopologyCanvas } from "@/components/khotan/topology-canvas";
|
|
2
|
+
|
|
3
|
+
export default function KhotanGraphPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(248,250,252,0.94),_rgba(241,245,249,0.82)_38%,_rgba(226,232,240,0.7))]">
|
|
6
|
+
<div className="mx-auto max-w-[1720px] px-4 py-8 md:px-6 xl:py-10">
|
|
7
|
+
<KhotanTopologyCanvas />
|
|
8
|
+
</div>
|
|
9
|
+
</main>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from "@/components/ui/table";
|
|
14
|
+
import { Switch } from "@/components/ui/switch";
|
|
15
|
+
import { Button } from "@/components/ui/button";
|
|
16
|
+
import { WirePanel } from "./wire";
|
|
17
|
+
import { VarPanel } from "./var-panel";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Khotan Hub — Dashboard for configured plugs and flows
|
|
21
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
22
|
+
//
|
|
23
|
+
// This file is yours. Restyle, extend, or embed it however you like.
|
|
24
|
+
// It uses shadcn/ui primitives and fetches from /api/khotan.
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
interface Plug {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
authType: string;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
status: "connected" | "error" | "idle";
|
|
34
|
+
statusMessage: string | null;
|
|
35
|
+
flowCount: number;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
updatedAt: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Flow {
|
|
41
|
+
id: string;
|
|
42
|
+
plugId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
type: "inflow" | "outflow" | "relay" | "webhook";
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
schedule: string | null;
|
|
47
|
+
lastRunAt: string | null;
|
|
48
|
+
lastRunStatus: "completed" | "partial" | "failed" | "cancelled" | null;
|
|
49
|
+
plugName: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface WebhookHandler {
|
|
53
|
+
id: string;
|
|
54
|
+
wireId: string;
|
|
55
|
+
name: string;
|
|
56
|
+
type: "catch" | "pass";
|
|
57
|
+
destinationPlugId: string | null;
|
|
58
|
+
events: string[] | null;
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
lastRunAt: string | null;
|
|
61
|
+
lastRunStatus:
|
|
62
|
+
| "pending"
|
|
63
|
+
| "running"
|
|
64
|
+
| "completed"
|
|
65
|
+
| "partial"
|
|
66
|
+
| "failed"
|
|
67
|
+
| "cancelled"
|
|
68
|
+
| null;
|
|
69
|
+
createdAt: string;
|
|
70
|
+
updatedAt: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type StatusVariant = "default" | "secondary" | "destructive" | "outline";
|
|
74
|
+
type FlowTypeVariant = "default" | "secondary" | "outline";
|
|
75
|
+
|
|
76
|
+
const statusVariant: Record<string, StatusVariant> = {
|
|
77
|
+
connected: "default",
|
|
78
|
+
idle: "secondary",
|
|
79
|
+
error: "destructive",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const flowTypeVariant: Record<string, FlowTypeVariant> = {
|
|
83
|
+
inflow: "default",
|
|
84
|
+
outflow: "secondary",
|
|
85
|
+
relay: "outline",
|
|
86
|
+
webhook: "outline",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const runStatusVariant: Record<string, StatusVariant> = {
|
|
90
|
+
pending: "outline",
|
|
91
|
+
running: "secondary",
|
|
92
|
+
completed: "default",
|
|
93
|
+
partial: "secondary",
|
|
94
|
+
failed: "destructive",
|
|
95
|
+
cancelled: "outline",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function KhotanHub({
|
|
99
|
+
webhookUrl,
|
|
100
|
+
debugHref,
|
|
101
|
+
logsHref = "/logs",
|
|
102
|
+
}: {
|
|
103
|
+
webhookUrl?: string;
|
|
104
|
+
debugHref?: (plugName: string) => string;
|
|
105
|
+
logsHref?: string;
|
|
106
|
+
} = {}) {
|
|
107
|
+
const [plugs, setPlugs] = useState<Plug[]>([]);
|
|
108
|
+
const [flows, setFlows] = useState<Flow[]>([]);
|
|
109
|
+
const [webhookHandlers, setWebhookHandlers] = useState<WebhookHandler[]>([]);
|
|
110
|
+
const [loading, setLoading] = useState(true);
|
|
111
|
+
const [error, setError] = useState<string | null>(null);
|
|
112
|
+
const [selectedPlugId, setSelectedPlugId] = useState<string | null>(null);
|
|
113
|
+
const [debugEnabled, setDebugEnabled] = useState(false);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
fetch("/api/khotan/debug")
|
|
117
|
+
.then((res) => setDebugEnabled(res.ok))
|
|
118
|
+
.catch(() => setDebugEnabled(false));
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
async function fetchData() {
|
|
122
|
+
setLoading(true);
|
|
123
|
+
setError(null);
|
|
124
|
+
try {
|
|
125
|
+
const [plugsRes, flowsRes] = await Promise.all([
|
|
126
|
+
fetch("/api/khotan/plugs"),
|
|
127
|
+
fetch("/api/khotan/flows"),
|
|
128
|
+
]);
|
|
129
|
+
if (!plugsRes.ok || !flowsRes.ok) {
|
|
130
|
+
throw new Error("Failed to fetch khotan data");
|
|
131
|
+
}
|
|
132
|
+
setPlugs(await plugsRes.json());
|
|
133
|
+
setFlows(await flowsRes.json());
|
|
134
|
+
} catch (err) {
|
|
135
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
136
|
+
} finally {
|
|
137
|
+
setLoading(false);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function toggleFlow(flowId: string, enabled: boolean) {
|
|
142
|
+
setFlows((prev) =>
|
|
143
|
+
prev.map((flow) => (flow.id === flowId ? { ...flow, enabled } : flow)),
|
|
144
|
+
);
|
|
145
|
+
await fetch(`/api/khotan/flows/${flowId}`, {
|
|
146
|
+
method: "PATCH",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({ enabled }),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function toggleWebhookHandler(handlerId: string, enabled: boolean) {
|
|
153
|
+
setWebhookHandlers((prev) =>
|
|
154
|
+
prev.map((h) => (h.id === handlerId ? { ...h, enabled } : h)),
|
|
155
|
+
);
|
|
156
|
+
await fetch(`/api/khotan/webhook-handlers/${handlerId}`, {
|
|
157
|
+
method: "PATCH",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ enabled }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
fetchData();
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!selectedPlugId) {
|
|
169
|
+
setWebhookHandlers([]);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const plug = plugs.find((p) => p.id === selectedPlugId);
|
|
173
|
+
if (!plug) return;
|
|
174
|
+
fetch(`/api/khotan/webhook-handlers/${plug.name}`)
|
|
175
|
+
.then((res) => (res.ok ? res.json() : []))
|
|
176
|
+
.then((data) => setWebhookHandlers(data))
|
|
177
|
+
.catch(() => setWebhookHandlers([]));
|
|
178
|
+
}, [selectedPlugId, plugs]);
|
|
179
|
+
|
|
180
|
+
if (loading) {
|
|
181
|
+
return (
|
|
182
|
+
<div className="space-y-4">
|
|
183
|
+
<div className="h-8 w-48 animate-pulse rounded bg-muted" />
|
|
184
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
185
|
+
{[1, 2, 3].map((i) => (
|
|
186
|
+
<Card key={i}>
|
|
187
|
+
<CardHeader>
|
|
188
|
+
<div className="h-5 w-32 animate-pulse rounded bg-muted" />
|
|
189
|
+
</CardHeader>
|
|
190
|
+
<CardContent>
|
|
191
|
+
<div className="space-y-2">
|
|
192
|
+
<div className="h-4 w-full animate-pulse rounded bg-muted" />
|
|
193
|
+
<div className="h-4 w-2/3 animate-pulse rounded bg-muted" />
|
|
194
|
+
</div>
|
|
195
|
+
</CardContent>
|
|
196
|
+
</Card>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (error) {
|
|
204
|
+
return (
|
|
205
|
+
<Card>
|
|
206
|
+
<CardContent className="py-8 text-center">
|
|
207
|
+
<p className="text-destructive mb-4">{error}</p>
|
|
208
|
+
<button
|
|
209
|
+
onClick={fetchData}
|
|
210
|
+
className="text-sm underline hover:no-underline"
|
|
211
|
+
>
|
|
212
|
+
Retry
|
|
213
|
+
</button>
|
|
214
|
+
</CardContent>
|
|
215
|
+
</Card>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (plugs.length === 0) {
|
|
220
|
+
return (
|
|
221
|
+
<Card>
|
|
222
|
+
<CardContent className="py-8 text-center">
|
|
223
|
+
<p className="text-muted-foreground mb-2">No plugs configured yet.</p>
|
|
224
|
+
<p className="text-sm text-muted-foreground">
|
|
225
|
+
Register plugs in your <code>khotan.ts</code> config file, then
|
|
226
|
+
restart the dev server.
|
|
227
|
+
</p>
|
|
228
|
+
</CardContent>
|
|
229
|
+
</Card>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const selectedPlug = selectedPlugId
|
|
234
|
+
? plugs.find((p) => p.id === selectedPlugId)
|
|
235
|
+
: null;
|
|
236
|
+
const plugFlows = selectedPlugId
|
|
237
|
+
? flows.filter((flow) => flow.plugId === selectedPlugId)
|
|
238
|
+
: [];
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div className="space-y-6">
|
|
242
|
+
<div className="flex items-center justify-between gap-4">
|
|
243
|
+
<h2 className="text-2xl font-bold tracking-tight">Khotan Hub</h2>
|
|
244
|
+
<Button
|
|
245
|
+
variant="outline"
|
|
246
|
+
size="sm"
|
|
247
|
+
onClick={() => {
|
|
248
|
+
window.location.href = logsHref;
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
Open Logs
|
|
252
|
+
</Button>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
256
|
+
{plugs.map((plug) => (
|
|
257
|
+
<Card
|
|
258
|
+
key={plug.id}
|
|
259
|
+
className={`cursor-pointer transition-colors hover:border-primary ${
|
|
260
|
+
selectedPlugId === plug.id ? "border-primary" : ""
|
|
261
|
+
}`}
|
|
262
|
+
onClick={() =>
|
|
263
|
+
setSelectedPlugId(selectedPlugId === plug.id ? null : plug.id)
|
|
264
|
+
}
|
|
265
|
+
>
|
|
266
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
267
|
+
<CardTitle className="text-sm font-medium">{plug.name}</CardTitle>
|
|
268
|
+
<Badge variant={statusVariant[plug.status] ?? "secondary"}>
|
|
269
|
+
{plug.status}
|
|
270
|
+
</Badge>
|
|
271
|
+
</CardHeader>
|
|
272
|
+
<CardContent>
|
|
273
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
274
|
+
{plug.baseUrl}
|
|
275
|
+
</p>
|
|
276
|
+
<div className="mt-2 flex items-center justify-between">
|
|
277
|
+
<p className="text-xs text-muted-foreground">
|
|
278
|
+
{plug.authType} · {plug.flowCount} flow
|
|
279
|
+
{plug.flowCount !== 1 ? "s" : ""}
|
|
280
|
+
</p>
|
|
281
|
+
{debugEnabled && (
|
|
282
|
+
<Button
|
|
283
|
+
size="sm"
|
|
284
|
+
variant="ghost"
|
|
285
|
+
className="h-6 px-2 text-[11px]"
|
|
286
|
+
onClick={(e) => {
|
|
287
|
+
e.stopPropagation();
|
|
288
|
+
if (debugHref) {
|
|
289
|
+
window.location.href = debugHref(plug.name);
|
|
290
|
+
} else {
|
|
291
|
+
window.location.href = `/debug/${plug.name}`;
|
|
292
|
+
}
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
Debug
|
|
296
|
+
</Button>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</CardContent>
|
|
300
|
+
</Card>
|
|
301
|
+
))}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{selectedPlug && (
|
|
305
|
+
<div className="space-y-4">
|
|
306
|
+
<Card>
|
|
307
|
+
<CardHeader>
|
|
308
|
+
<CardTitle>{selectedPlug.name} — Flows</CardTitle>
|
|
309
|
+
</CardHeader>
|
|
310
|
+
<CardContent>
|
|
311
|
+
{plugFlows.length === 0 ? (
|
|
312
|
+
<p className="text-sm text-muted-foreground">
|
|
313
|
+
No flows configured for this plug.
|
|
314
|
+
</p>
|
|
315
|
+
) : (
|
|
316
|
+
<Table>
|
|
317
|
+
<TableHeader>
|
|
318
|
+
<TableRow>
|
|
319
|
+
<TableHead>Name</TableHead>
|
|
320
|
+
<TableHead>Type</TableHead>
|
|
321
|
+
<TableHead>Schedule</TableHead>
|
|
322
|
+
<TableHead>Last Run</TableHead>
|
|
323
|
+
<TableHead>Enabled</TableHead>
|
|
324
|
+
</TableRow>
|
|
325
|
+
</TableHeader>
|
|
326
|
+
<TableBody>
|
|
327
|
+
{plugFlows.map((flow) => (
|
|
328
|
+
<TableRow key={flow.id}>
|
|
329
|
+
<TableCell className="font-medium">
|
|
330
|
+
{flow.name}
|
|
331
|
+
</TableCell>
|
|
332
|
+
<TableCell>
|
|
333
|
+
<Badge
|
|
334
|
+
variant={flowTypeVariant[flow.type] ?? "outline"}
|
|
335
|
+
>
|
|
336
|
+
{flow.type}
|
|
337
|
+
</Badge>
|
|
338
|
+
</TableCell>
|
|
339
|
+
<TableCell className="text-muted-foreground">
|
|
340
|
+
{flow.schedule ?? "—"}
|
|
341
|
+
</TableCell>
|
|
342
|
+
<TableCell>
|
|
343
|
+
{flow.lastRunStatus ? (
|
|
344
|
+
<Badge
|
|
345
|
+
variant={
|
|
346
|
+
runStatusVariant[flow.lastRunStatus] ??
|
|
347
|
+
"secondary"
|
|
348
|
+
}
|
|
349
|
+
>
|
|
350
|
+
{flow.lastRunStatus}
|
|
351
|
+
</Badge>
|
|
352
|
+
) : (
|
|
353
|
+
<span className="text-muted-foreground">never</span>
|
|
354
|
+
)}
|
|
355
|
+
</TableCell>
|
|
356
|
+
<TableCell>
|
|
357
|
+
<Switch
|
|
358
|
+
checked={flow.enabled}
|
|
359
|
+
onCheckedChange={(v) => toggleFlow(flow.id, v)}
|
|
360
|
+
/>
|
|
361
|
+
</TableCell>
|
|
362
|
+
</TableRow>
|
|
363
|
+
))}
|
|
364
|
+
</TableBody>
|
|
365
|
+
</Table>
|
|
366
|
+
)}
|
|
367
|
+
</CardContent>
|
|
368
|
+
</Card>
|
|
369
|
+
|
|
370
|
+
{webhookHandlers.length > 0 && (
|
|
371
|
+
<Card>
|
|
372
|
+
<CardHeader>
|
|
373
|
+
<CardTitle>{selectedPlug.name} — Webhook Handlers</CardTitle>
|
|
374
|
+
</CardHeader>
|
|
375
|
+
<CardContent>
|
|
376
|
+
<Table>
|
|
377
|
+
<TableHeader>
|
|
378
|
+
<TableRow>
|
|
379
|
+
<TableHead>Name</TableHead>
|
|
380
|
+
<TableHead>Type</TableHead>
|
|
381
|
+
<TableHead>Events</TableHead>
|
|
382
|
+
<TableHead>Destination</TableHead>
|
|
383
|
+
<TableHead>Last Run</TableHead>
|
|
384
|
+
<TableHead>Enabled</TableHead>
|
|
385
|
+
</TableRow>
|
|
386
|
+
</TableHeader>
|
|
387
|
+
<TableBody>
|
|
388
|
+
{webhookHandlers.map((handler) => (
|
|
389
|
+
<TableRow key={handler.id}>
|
|
390
|
+
<TableCell className="font-medium">
|
|
391
|
+
{handler.name}
|
|
392
|
+
</TableCell>
|
|
393
|
+
<TableCell>
|
|
394
|
+
<Badge
|
|
395
|
+
variant={
|
|
396
|
+
handler.type === "catch" ? "default" : "secondary"
|
|
397
|
+
}
|
|
398
|
+
>
|
|
399
|
+
{handler.type}
|
|
400
|
+
</Badge>
|
|
401
|
+
</TableCell>
|
|
402
|
+
<TableCell className="text-muted-foreground">
|
|
403
|
+
{handler.events?.length
|
|
404
|
+
? handler.events.join(", ")
|
|
405
|
+
: "from wire config"}
|
|
406
|
+
</TableCell>
|
|
407
|
+
<TableCell className="text-muted-foreground">
|
|
408
|
+
{handler.destinationPlugId
|
|
409
|
+
? (plugs.find(
|
|
410
|
+
(p) => p.id === handler.destinationPlugId,
|
|
411
|
+
)?.name ?? handler.destinationPlugId)
|
|
412
|
+
: "—"}
|
|
413
|
+
</TableCell>
|
|
414
|
+
<TableCell>
|
|
415
|
+
{handler.lastRunStatus ? (
|
|
416
|
+
<Badge
|
|
417
|
+
variant={
|
|
418
|
+
runStatusVariant[handler.lastRunStatus] ??
|
|
419
|
+
"secondary"
|
|
420
|
+
}
|
|
421
|
+
>
|
|
422
|
+
{handler.lastRunStatus}
|
|
423
|
+
</Badge>
|
|
424
|
+
) : (
|
|
425
|
+
<span className="text-muted-foreground">never</span>
|
|
426
|
+
)}
|
|
427
|
+
</TableCell>
|
|
428
|
+
<TableCell>
|
|
429
|
+
<Switch
|
|
430
|
+
checked={handler.enabled}
|
|
431
|
+
onCheckedChange={(v) =>
|
|
432
|
+
toggleWebhookHandler(handler.id, v)
|
|
433
|
+
}
|
|
434
|
+
/>
|
|
435
|
+
</TableCell>
|
|
436
|
+
</TableRow>
|
|
437
|
+
))}
|
|
438
|
+
</TableBody>
|
|
439
|
+
</Table>
|
|
440
|
+
</CardContent>
|
|
441
|
+
</Card>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
<VarPanel plugName={selectedPlug.name} />
|
|
445
|
+
<WirePanel plugName={selectedPlug.name} webhookUrl={webhookUrl} />
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Example: Inflow
|
|
3
|
+
// Generated by khotan CLI · https://github.com/khotan-data
|
|
4
|
+
//
|
|
5
|
+
// Copy this file, rename it for your source service/resource, and register the
|
|
6
|
+
// exported flow in {outputDir}/khotan.ts.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
import { inflow, type InflowContext } from "./inflow";
|
|
10
|
+
import { sendUpdate } from "khotan-data/factory";
|
|
11
|
+
|
|
12
|
+
async function shopifyProductsWorkflow(ctx: InflowContext) {
|
|
13
|
+
"use workflow";
|
|
14
|
+
|
|
15
|
+
async function extractAndLoad() {
|
|
16
|
+
"use step";
|
|
17
|
+
console.log("Starting inflow", {
|
|
18
|
+
flow: ctx.flow.name,
|
|
19
|
+
khotanRunId: ctx.khotanRunId,
|
|
20
|
+
runType: ctx.runType,
|
|
21
|
+
});
|
|
22
|
+
await sendUpdate({
|
|
23
|
+
message: "Starting product inflow",
|
|
24
|
+
metadata: { flow: ctx.flow.name, runType: ctx.runType },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const response = await fetch("https://api.example.com/products", {
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${ctx.vars["apiToken"] ?? ""}`,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const payload = (await response.json()) as {
|
|
33
|
+
data?: Array<Record<string, unknown>>;
|
|
34
|
+
};
|
|
35
|
+
const records = Array.isArray(payload.data) ? payload.data : [];
|
|
36
|
+
|
|
37
|
+
// Replace this with your app-specific transform and DB upsert.
|
|
38
|
+
console.log("Fetched records", records.length);
|
|
39
|
+
await sendUpdate({
|
|
40
|
+
message: `Fetched ${String(records.length)} products`,
|
|
41
|
+
extracted: records.length,
|
|
42
|
+
progress: 50,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
extracted: records.length,
|
|
47
|
+
transformed: records.length,
|
|
48
|
+
created: records.length,
|
|
49
|
+
metadata: { source: ctx.flow.name },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return extractAndLoad();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const shopifyProductsInflow = inflow({
|
|
57
|
+
name: "shopify-products-inflow",
|
|
58
|
+
resource: "products",
|
|
59
|
+
schedule: "0 * * * *",
|
|
60
|
+
workflow: shopifyProductsWorkflow,
|
|
61
|
+
});
|