apteva 0.4.4 → 0.4.6
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/dist/App.csbvbyak.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/db.ts +98 -19
- package/src/integrations/agentdojo.ts +350 -0
- package/src/openapi.ts +195 -0
- package/src/providers.ts +78 -7
- package/src/routes/api/agent-utils.ts +638 -0
- package/src/routes/api/agents.ts +743 -0
- package/src/routes/api/helpers.ts +12 -0
- package/src/routes/api/integrations.ts +608 -0
- package/src/routes/api/mcp.ts +377 -0
- package/src/routes/api/meta-agent.ts +145 -0
- package/src/routes/api/projects.ts +95 -0
- package/src/routes/api/providers.ts +269 -0
- package/src/routes/api/skills.ts +538 -0
- package/src/routes/api/system.ts +215 -0
- package/src/routes/api/telemetry.ts +143 -0
- package/src/routes/api/users.ts +148 -0
- package/src/routes/api.ts +32 -3477
- package/src/server.ts +1 -1
- package/src/web/components/api/ApiDocsPage.tsx +259 -0
- package/src/web/components/dashboard/Dashboard.tsx +92 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +15 -8
- package/src/web/components/mcp/McpPage.tsx +458 -174
- package/src/web/components/settings/SettingsPage.tsx +275 -36
- package/src/web/components/skills/SkillsPage.tsx +330 -1
- package/src/web/components/tasks/TasksPage.tsx +187 -58
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/hooks/useAgents.ts +9 -0
- package/src/web/types.ts +22 -4
- package/dist/App.mbp9atpm.js +0 -227
package/src/server.ts
CHANGED
|
@@ -421,7 +421,7 @@ if (hasRestarts) {
|
|
|
421
421
|
// Restart in background to not block startup
|
|
422
422
|
(async () => {
|
|
423
423
|
// Import startAgentProcess dynamically to avoid circular dependency
|
|
424
|
-
const { startAgentProcess } = await import("./routes/api");
|
|
424
|
+
const { startAgentProcess } = await import("./routes/api/agent-utils");
|
|
425
425
|
|
|
426
426
|
// Restart MCP servers first (agents may depend on them)
|
|
427
427
|
if (mcpServersToRestart.length > 0) {
|
|
@@ -51,6 +51,256 @@ const METHOD_COLORS: Record<string, string> = {
|
|
|
51
51
|
patch: "#50e3c2",
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
// Try It Out component
|
|
55
|
+
function TryItOut({
|
|
56
|
+
method,
|
|
57
|
+
path,
|
|
58
|
+
parameters,
|
|
59
|
+
requestBody,
|
|
60
|
+
authFetch,
|
|
61
|
+
}: {
|
|
62
|
+
method: string;
|
|
63
|
+
path: string;
|
|
64
|
+
parameters?: Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
in: string;
|
|
67
|
+
required?: boolean;
|
|
68
|
+
schema?: { type: string };
|
|
69
|
+
}>;
|
|
70
|
+
requestBody?: any;
|
|
71
|
+
authFetch: (url: string, options?: RequestInit) => Promise<Response>;
|
|
72
|
+
}) {
|
|
73
|
+
const [paramValues, setParamValues] = useState<Record<string, string>>({});
|
|
74
|
+
const [bodyValue, setBodyValue] = useState("");
|
|
75
|
+
const [response, setResponse] = useState<{ status: number; data: any } | null>(null);
|
|
76
|
+
const [loading, setLoading] = useState(false);
|
|
77
|
+
const [error, setError] = useState<string | null>(null);
|
|
78
|
+
|
|
79
|
+
// Initialize body with example if available
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (requestBody?.content?.["application/json"]?.schema) {
|
|
82
|
+
const schema = requestBody.content["application/json"].schema;
|
|
83
|
+
if (schema.example) {
|
|
84
|
+
setBodyValue(JSON.stringify(schema.example, null, 2));
|
|
85
|
+
} else if (schema.properties) {
|
|
86
|
+
// Generate example from properties
|
|
87
|
+
const example: Record<string, any> = {};
|
|
88
|
+
for (const [key, prop] of Object.entries(schema.properties) as [string, any][]) {
|
|
89
|
+
if (prop.example !== undefined) {
|
|
90
|
+
example[key] = prop.example;
|
|
91
|
+
} else if (prop.type === "string") {
|
|
92
|
+
example[key] = "";
|
|
93
|
+
} else if (prop.type === "number" || prop.type === "integer") {
|
|
94
|
+
example[key] = 0;
|
|
95
|
+
} else if (prop.type === "boolean") {
|
|
96
|
+
example[key] = false;
|
|
97
|
+
} else if (prop.type === "array") {
|
|
98
|
+
example[key] = [];
|
|
99
|
+
} else if (prop.type === "object") {
|
|
100
|
+
example[key] = {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
setBodyValue(JSON.stringify(example, null, 2));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [requestBody]);
|
|
107
|
+
|
|
108
|
+
const execute = async () => {
|
|
109
|
+
setLoading(true);
|
|
110
|
+
setError(null);
|
|
111
|
+
setResponse(null);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Build URL with path parameters
|
|
115
|
+
let url = path;
|
|
116
|
+
const queryParams: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const param of parameters || []) {
|
|
119
|
+
const value = paramValues[param.name] || "";
|
|
120
|
+
if (param.in === "path") {
|
|
121
|
+
url = url.replace(`{${param.name}}`, encodeURIComponent(value));
|
|
122
|
+
} else if (param.in === "query" && value) {
|
|
123
|
+
queryParams.push(`${param.name}=${encodeURIComponent(value)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (queryParams.length > 0) {
|
|
128
|
+
url += `?${queryParams.join("&")}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const options: RequestInit = {
|
|
132
|
+
method: method.toUpperCase(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (bodyValue && ["post", "put", "patch"].includes(method)) {
|
|
136
|
+
options.headers = { "Content-Type": "application/json" };
|
|
137
|
+
options.body = bodyValue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const res = await authFetch(`/api${url}`, options);
|
|
141
|
+
let data;
|
|
142
|
+
const contentType = res.headers.get("content-type");
|
|
143
|
+
if (contentType?.includes("application/json")) {
|
|
144
|
+
data = await res.json();
|
|
145
|
+
} else {
|
|
146
|
+
data = await res.text();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setResponse({ status: res.status, data });
|
|
150
|
+
} catch (err: any) {
|
|
151
|
+
setError(err.message || "Request failed");
|
|
152
|
+
} finally {
|
|
153
|
+
setLoading(false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const pathParams = parameters?.filter((p) => p.in === "path") || [];
|
|
158
|
+
const queryParams = parameters?.filter((p) => p.in === "query") || [];
|
|
159
|
+
const hasBody = ["post", "put", "patch"].includes(method) && requestBody;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div style={{ marginTop: 16, padding: 16, background: "#0a0a14", borderRadius: 6, border: "1px solid #222" }}>
|
|
163
|
+
<h4 style={{ fontSize: 13, color: "#f97316", marginBottom: 12, fontWeight: 600 }}>Try it out</h4>
|
|
164
|
+
|
|
165
|
+
{/* Path Parameters */}
|
|
166
|
+
{pathParams.length > 0 && (
|
|
167
|
+
<div style={{ marginBottom: 12 }}>
|
|
168
|
+
<div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>Path Parameters</div>
|
|
169
|
+
{pathParams.map((param) => (
|
|
170
|
+
<div key={param.name} style={{ marginBottom: 8 }}>
|
|
171
|
+
<label style={{ fontSize: 12, color: "#888", display: "block", marginBottom: 4 }}>
|
|
172
|
+
{param.name} {param.required && <span style={{ color: "#f66" }}>*</span>}
|
|
173
|
+
</label>
|
|
174
|
+
<input
|
|
175
|
+
type="text"
|
|
176
|
+
value={paramValues[param.name] || ""}
|
|
177
|
+
onChange={(e) => setParamValues({ ...paramValues, [param.name]: e.target.value })}
|
|
178
|
+
placeholder={param.schema?.type || "string"}
|
|
179
|
+
style={{
|
|
180
|
+
width: "100%",
|
|
181
|
+
padding: "8px 12px",
|
|
182
|
+
background: "#111",
|
|
183
|
+
border: "1px solid #333",
|
|
184
|
+
borderRadius: 4,
|
|
185
|
+
color: "#fff",
|
|
186
|
+
fontSize: 13,
|
|
187
|
+
fontFamily: "monospace",
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{/* Query Parameters */}
|
|
196
|
+
{queryParams.length > 0 && (
|
|
197
|
+
<div style={{ marginBottom: 12 }}>
|
|
198
|
+
<div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>Query Parameters</div>
|
|
199
|
+
{queryParams.map((param) => (
|
|
200
|
+
<div key={param.name} style={{ marginBottom: 8 }}>
|
|
201
|
+
<label style={{ fontSize: 12, color: "#888", display: "block", marginBottom: 4 }}>
|
|
202
|
+
{param.name} {param.required && <span style={{ color: "#f66" }}>*</span>}
|
|
203
|
+
</label>
|
|
204
|
+
<input
|
|
205
|
+
type="text"
|
|
206
|
+
value={paramValues[param.name] || ""}
|
|
207
|
+
onChange={(e) => setParamValues({ ...paramValues, [param.name]: e.target.value })}
|
|
208
|
+
placeholder={param.schema?.type || "string"}
|
|
209
|
+
style={{
|
|
210
|
+
width: "100%",
|
|
211
|
+
padding: "8px 12px",
|
|
212
|
+
background: "#111",
|
|
213
|
+
border: "1px solid #333",
|
|
214
|
+
borderRadius: 4,
|
|
215
|
+
color: "#fff",
|
|
216
|
+
fontSize: 13,
|
|
217
|
+
fontFamily: "monospace",
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{/* Request Body */}
|
|
226
|
+
{hasBody && (
|
|
227
|
+
<div style={{ marginBottom: 12 }}>
|
|
228
|
+
<div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>Request Body (JSON)</div>
|
|
229
|
+
<textarea
|
|
230
|
+
value={bodyValue}
|
|
231
|
+
onChange={(e) => setBodyValue(e.target.value)}
|
|
232
|
+
rows={6}
|
|
233
|
+
style={{
|
|
234
|
+
width: "100%",
|
|
235
|
+
padding: "8px 12px",
|
|
236
|
+
background: "#111",
|
|
237
|
+
border: "1px solid #333",
|
|
238
|
+
borderRadius: 4,
|
|
239
|
+
color: "#fff",
|
|
240
|
+
fontSize: 12,
|
|
241
|
+
fontFamily: "monospace",
|
|
242
|
+
resize: "vertical",
|
|
243
|
+
}}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{/* Execute Button */}
|
|
249
|
+
<button
|
|
250
|
+
onClick={execute}
|
|
251
|
+
disabled={loading}
|
|
252
|
+
style={{
|
|
253
|
+
padding: "10px 20px",
|
|
254
|
+
background: loading ? "#333" : "#f97316",
|
|
255
|
+
color: loading ? "#666" : "#000",
|
|
256
|
+
border: "none",
|
|
257
|
+
borderRadius: 4,
|
|
258
|
+
cursor: loading ? "not-allowed" : "pointer",
|
|
259
|
+
fontSize: 13,
|
|
260
|
+
fontWeight: 600,
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{loading ? "Executing..." : "Execute"}
|
|
264
|
+
</button>
|
|
265
|
+
|
|
266
|
+
{/* Error */}
|
|
267
|
+
{error && (
|
|
268
|
+
<div style={{ marginTop: 12, padding: 12, background: "#2a1515", borderRadius: 4, color: "#f66", fontSize: 12 }}>
|
|
269
|
+
{error}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* Response */}
|
|
274
|
+
{response && (
|
|
275
|
+
<div style={{ marginTop: 12 }}>
|
|
276
|
+
<div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>
|
|
277
|
+
Response{" "}
|
|
278
|
+
<span style={{ color: response.status >= 200 && response.status < 300 ? "#49cc90" : "#f66" }}>
|
|
279
|
+
{response.status}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
<pre
|
|
283
|
+
style={{
|
|
284
|
+
padding: 12,
|
|
285
|
+
background: "#111",
|
|
286
|
+
borderRadius: 4,
|
|
287
|
+
color: "#888",
|
|
288
|
+
fontSize: 11,
|
|
289
|
+
fontFamily: "monospace",
|
|
290
|
+
overflow: "auto",
|
|
291
|
+
maxHeight: 300,
|
|
292
|
+
whiteSpace: "pre-wrap",
|
|
293
|
+
wordBreak: "break-word",
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{typeof response.data === "string" ? response.data : JSON.stringify(response.data, null, 2)}
|
|
297
|
+
</pre>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
54
304
|
export function ApiDocsPage() {
|
|
55
305
|
const { authFetch } = useAuth();
|
|
56
306
|
const [spec, setSpec] = useState<OpenApiSpec | null>(null);
|
|
@@ -516,6 +766,15 @@ export function ApiDocsPage() {
|
|
|
516
766
|
</div>
|
|
517
767
|
</div>
|
|
518
768
|
)}
|
|
769
|
+
|
|
770
|
+
{/* Try It Out */}
|
|
771
|
+
<TryItOut
|
|
772
|
+
method={method}
|
|
773
|
+
path={path}
|
|
774
|
+
parameters={details.parameters}
|
|
775
|
+
requestBody={details.requestBody}
|
|
776
|
+
authFetch={authFetch}
|
|
777
|
+
/>
|
|
519
778
|
</div>
|
|
520
779
|
)}
|
|
521
780
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
|
-
import { useAgentActivity, useAuth, useProjects } from "../../context";
|
|
2
|
+
import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
|
|
3
|
+
import type { TelemetryEvent } from "../../context";
|
|
3
4
|
import type { Agent, Provider, Route, DashboardStats, Task } from "../../types";
|
|
4
5
|
|
|
5
6
|
interface DashboardProps {
|
|
@@ -21,8 +22,10 @@ export function Dashboard({
|
|
|
21
22
|
}: DashboardProps) {
|
|
22
23
|
const { authFetch } = useAuth();
|
|
23
24
|
const { currentProjectId } = useProjects();
|
|
25
|
+
const { events: realtimeEvents } = useTelemetryContext();
|
|
24
26
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
25
27
|
const [recentTasks, setRecentTasks] = useState<Task[]>([]);
|
|
28
|
+
const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
|
|
26
29
|
|
|
27
30
|
// Filter agents by current project
|
|
28
31
|
const filteredAgents = useMemo(() => {
|
|
@@ -42,9 +45,10 @@ export function Dashboard({
|
|
|
42
45
|
|
|
43
46
|
const fetchDashboardData = useCallback(async () => {
|
|
44
47
|
try {
|
|
45
|
-
const [dashRes, tasksRes] = await Promise.all([
|
|
48
|
+
const [dashRes, tasksRes, activityRes] = await Promise.all([
|
|
46
49
|
authFetch("/api/dashboard"),
|
|
47
50
|
authFetch("/api/tasks?status=all"),
|
|
51
|
+
authFetch("/api/telemetry/events?type=thread_activity&limit=20"),
|
|
48
52
|
]);
|
|
49
53
|
|
|
50
54
|
if (dashRes.ok) {
|
|
@@ -56,6 +60,11 @@ export function Dashboard({
|
|
|
56
60
|
const data = await tasksRes.json();
|
|
57
61
|
setRecentTasks((data.tasks || []).slice(0, 5));
|
|
58
62
|
}
|
|
63
|
+
|
|
64
|
+
if (activityRes.ok) {
|
|
65
|
+
const data = await activityRes.json();
|
|
66
|
+
setHistoricalActivities(data.events || []);
|
|
67
|
+
}
|
|
59
68
|
} catch (e) {
|
|
60
69
|
console.error("Failed to fetch dashboard data:", e);
|
|
61
70
|
}
|
|
@@ -86,6 +95,36 @@ export function Dashboard({
|
|
|
86
95
|
return { total, pending, running, completed };
|
|
87
96
|
}, [stats, currentProjectId, filteredTasks]);
|
|
88
97
|
|
|
98
|
+
// Merge real-time + historical thread_activity events, deduplicate
|
|
99
|
+
const activities = useMemo(() => {
|
|
100
|
+
const realtimeActivities = realtimeEvents.filter(e => e.type === "thread_activity");
|
|
101
|
+
const seen = new Set(realtimeActivities.map(e => e.id));
|
|
102
|
+
const merged = [...realtimeActivities];
|
|
103
|
+
for (const evt of historicalActivities) {
|
|
104
|
+
if (!seen.has(evt.id)) {
|
|
105
|
+
merged.push(evt);
|
|
106
|
+
seen.add(evt.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Filter by project
|
|
110
|
+
let filtered = merged;
|
|
111
|
+
if (currentProjectId) {
|
|
112
|
+
filtered = merged.filter(e => projectAgentIds.has(e.agent_id));
|
|
113
|
+
}
|
|
114
|
+
// Sort newest first
|
|
115
|
+
filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
116
|
+
return filtered.slice(0, 8);
|
|
117
|
+
}, [realtimeEvents, historicalActivities, currentProjectId, projectAgentIds]);
|
|
118
|
+
|
|
119
|
+
// Build agent name lookup
|
|
120
|
+
const agentNameMap = useMemo(() => {
|
|
121
|
+
const map = new Map<string, string>();
|
|
122
|
+
for (const a of agents) {
|
|
123
|
+
map.set(a.id, a.name);
|
|
124
|
+
}
|
|
125
|
+
return map;
|
|
126
|
+
}, [agents]);
|
|
127
|
+
|
|
89
128
|
return (
|
|
90
129
|
<div className="flex-1 overflow-auto p-6">
|
|
91
130
|
{/* Stats Cards */}
|
|
@@ -96,7 +135,7 @@ export function Dashboard({
|
|
|
96
135
|
<StatCard label="Providers" value={configuredProviders.length} color="text-[#f97316]" />
|
|
97
136
|
</div>
|
|
98
137
|
|
|
99
|
-
<div className="grid grid-cols-1 lg:grid-cols-
|
|
138
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
100
139
|
{/* Agents List */}
|
|
101
140
|
<DashboardCard
|
|
102
141
|
title="Agents"
|
|
@@ -116,6 +155,31 @@ export function Dashboard({
|
|
|
116
155
|
)}
|
|
117
156
|
</DashboardCard>
|
|
118
157
|
|
|
158
|
+
{/* Activity Feed */}
|
|
159
|
+
<DashboardCard
|
|
160
|
+
title="Activity"
|
|
161
|
+
actionLabel="Telemetry"
|
|
162
|
+
onAction={() => onNavigate("telemetry")}
|
|
163
|
+
>
|
|
164
|
+
{activities.length === 0 ? (
|
|
165
|
+
<div className="p-4 text-center text-[#666]">
|
|
166
|
+
<p>No activity yet</p>
|
|
167
|
+
<p className="text-sm text-[#444] mt-1">Agent activity will appear here in real-time</p>
|
|
168
|
+
</div>
|
|
169
|
+
) : (
|
|
170
|
+
<div className="divide-y divide-[#1a1a1a]">
|
|
171
|
+
{activities.map((evt) => (
|
|
172
|
+
<ActivityItem
|
|
173
|
+
key={evt.id}
|
|
174
|
+
activity={(evt.data?.activity as string) || "Working..."}
|
|
175
|
+
agentName={agentNameMap.get(evt.agent_id) || evt.agent_id}
|
|
176
|
+
timestamp={evt.timestamp}
|
|
177
|
+
/>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</DashboardCard>
|
|
182
|
+
|
|
119
183
|
{/* Recent Tasks */}
|
|
120
184
|
<DashboardCard
|
|
121
185
|
title="Recent Tasks"
|
|
@@ -228,6 +292,31 @@ function AgentListItem({ agent, onSelect, showProject }: { agent: Agent; onSelec
|
|
|
228
292
|
);
|
|
229
293
|
}
|
|
230
294
|
|
|
295
|
+
function timeAgo(timestamp: string): string {
|
|
296
|
+
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
297
|
+
if (seconds < 5) return "just now";
|
|
298
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
299
|
+
const minutes = Math.floor(seconds / 60);
|
|
300
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
301
|
+
const hours = Math.floor(minutes / 60);
|
|
302
|
+
if (hours < 24) return `${hours}h ago`;
|
|
303
|
+
const days = Math.floor(hours / 24);
|
|
304
|
+
return `${days}d ago`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function ActivityItem({ activity, agentName, timestamp }: { activity: string; agentName: string; timestamp: string }) {
|
|
308
|
+
return (
|
|
309
|
+
<div className="px-4 py-3">
|
|
310
|
+
<p className="text-sm truncate">{activity}</p>
|
|
311
|
+
<div className="flex items-center gap-2 text-xs text-[#555] mt-1">
|
|
312
|
+
<span className="text-[#666]">{agentName}</span>
|
|
313
|
+
<span className="text-[#444]">·</span>
|
|
314
|
+
<span>{timeAgo(timestamp)}</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
231
320
|
function TaskStatusBadge({ status }: { status: Task["status"] }) {
|
|
232
321
|
const colors: Record<string, string> = {
|
|
233
322
|
pending: "bg-yellow-500/20 text-yellow-400",
|
|
@@ -44,9 +44,11 @@ function hasMultipleAuthMethods(app: IntegrationApp): boolean {
|
|
|
44
44
|
// Main component
|
|
45
45
|
export function IntegrationsPanel({
|
|
46
46
|
providerId = "composio",
|
|
47
|
+
projectId,
|
|
47
48
|
onConnectionComplete,
|
|
48
49
|
}: {
|
|
49
50
|
providerId?: string;
|
|
51
|
+
projectId?: string | null;
|
|
50
52
|
onConnectionComplete?: () => void;
|
|
51
53
|
}) {
|
|
52
54
|
const { authFetch } = useAuth();
|
|
@@ -80,10 +82,11 @@ export function IntegrationsPanel({
|
|
|
80
82
|
const fetchData = useCallback(async () => {
|
|
81
83
|
setLoading(true);
|
|
82
84
|
setError(null);
|
|
85
|
+
const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
|
|
83
86
|
try {
|
|
84
87
|
const [appsRes, connectedRes] = await Promise.all([
|
|
85
|
-
authFetch(`/api/integrations/${providerId}/apps`),
|
|
86
|
-
authFetch(`/api/integrations/${providerId}/connected`),
|
|
88
|
+
authFetch(`/api/integrations/${providerId}/apps${projectParam}`),
|
|
89
|
+
authFetch(`/api/integrations/${providerId}/connected${projectParam}`),
|
|
87
90
|
]);
|
|
88
91
|
|
|
89
92
|
const appsData = await appsRes.json();
|
|
@@ -96,7 +99,7 @@ export function IntegrationsPanel({
|
|
|
96
99
|
setError("Failed to load integrations");
|
|
97
100
|
}
|
|
98
101
|
setLoading(false);
|
|
99
|
-
}, [authFetch, providerId]);
|
|
102
|
+
}, [authFetch, providerId, projectId]);
|
|
100
103
|
|
|
101
104
|
useEffect(() => {
|
|
102
105
|
fetchData();
|
|
@@ -118,11 +121,12 @@ export function IntegrationsPanel({
|
|
|
118
121
|
// Poll for pending connection status
|
|
119
122
|
useEffect(() => {
|
|
120
123
|
if (!pendingConnection?.connectionId) return;
|
|
124
|
+
const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
|
|
121
125
|
|
|
122
126
|
const pollInterval = setInterval(async () => {
|
|
123
127
|
try {
|
|
124
128
|
const res = await authFetch(
|
|
125
|
-
`/api/integrations/${providerId}/connection/${pendingConnection.connectionId}`
|
|
129
|
+
`/api/integrations/${providerId}/connection/${pendingConnection.connectionId}${projectParam}`
|
|
126
130
|
);
|
|
127
131
|
const data = await res.json();
|
|
128
132
|
|
|
@@ -142,7 +146,7 @@ export function IntegrationsPanel({
|
|
|
142
146
|
}, 2000);
|
|
143
147
|
|
|
144
148
|
return () => clearInterval(pollInterval);
|
|
145
|
-
}, [pendingConnection, authFetch, providerId, fetchData, onConnectionComplete]);
|
|
149
|
+
}, [pendingConnection, authFetch, providerId, projectId, fetchData, onConnectionComplete]);
|
|
146
150
|
|
|
147
151
|
// Initiate connection
|
|
148
152
|
const connectApp = async (app: IntegrationApp, apiKey?: string, forceOAuth?: boolean) => {
|
|
@@ -172,7 +176,8 @@ export function IntegrationsPanel({
|
|
|
172
176
|
};
|
|
173
177
|
}
|
|
174
178
|
|
|
175
|
-
const
|
|
179
|
+
const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
|
|
180
|
+
const res = await authFetch(`/api/integrations/${providerId}/connect${projectParam}`, {
|
|
176
181
|
method: "POST",
|
|
177
182
|
headers: { "Content-Type": "application/json" },
|
|
178
183
|
body: JSON.stringify(body),
|
|
@@ -231,9 +236,10 @@ export function IntegrationsPanel({
|
|
|
231
236
|
|
|
232
237
|
// Disconnect (called after confirmation)
|
|
233
238
|
const disconnectApp = async (account: ConnectedAccount) => {
|
|
239
|
+
const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
|
|
234
240
|
try {
|
|
235
241
|
const res = await authFetch(
|
|
236
|
-
`/api/integrations/${providerId}/connection/${account.id}`,
|
|
242
|
+
`/api/integrations/${providerId}/connection/${account.id}${projectParam}`,
|
|
237
243
|
{ method: "DELETE" }
|
|
238
244
|
);
|
|
239
245
|
|
|
@@ -263,7 +269,8 @@ export function IntegrationsPanel({
|
|
|
263
269
|
setError(null);
|
|
264
270
|
|
|
265
271
|
try {
|
|
266
|
-
const
|
|
272
|
+
const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
|
|
273
|
+
const res = await authFetch(`/api/integrations/${providerId}/configs${projectParam}`, {
|
|
267
274
|
method: "POST",
|
|
268
275
|
headers: { "Content-Type": "application/json" },
|
|
269
276
|
body: JSON.stringify({
|