Skip to content

Commit 0eb2147

Browse files
authored
smoother integration detection (#71)
* smoother integration detection * fix race condition * better integrations
1 parent 20766d5 commit 0eb2147

File tree

3 files changed

+111
-3
lines changed

3 files changed

+111
-3
lines changed

components/workflow/node-config-panel.tsx

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
RefreshCw,
1010
Trash2,
1111
} from "lucide-react";
12-
import { useMemo, useRef, useState } from "react";
12+
import { useCallback, useMemo, useRef, useState } from "react";
1313
import { toast } from "sonner";
1414
import {
1515
AlertDialog,
@@ -38,6 +38,7 @@ import {
3838
edgesAtom,
3939
isGeneratingAtom,
4040
nodesAtom,
41+
pendingIntegrationNodesAtom,
4142
propertiesPanelActiveTabAtom,
4243
selectedEdgeAtom,
4344
selectedNodeAtom,
@@ -62,6 +63,11 @@ import { WorkflowRuns } from "./workflow-runs";
6263
const NON_ALPHANUMERIC_REGEX = /[^a-zA-Z0-9\s]/g;
6364
const WORD_SPLIT_REGEX = /\s+/;
6465

66+
// System actions that need integrations (not in plugin registry)
67+
const SYSTEM_ACTION_INTEGRATIONS: Record<string, IntegrationType> = {
68+
"Database Query": "database",
69+
};
70+
6571
// Multi-selection panel component
6672
const MultiSelectionPanel = ({
6773
selectedNodes,
@@ -154,13 +160,17 @@ export const PanelInner = () => {
154160
const setShowClearDialog = useSetAtom(showClearDialogAtom);
155161
const setShowDeleteDialog = useSetAtom(showDeleteDialogAtom);
156162
const clearNodeStatuses = useSetAtom(clearNodeStatusesAtom);
163+
const setPendingIntegrationNodes = useSetAtom(pendingIntegrationNodesAtom);
157164
const [showDeleteNodeAlert, setShowDeleteNodeAlert] = useState(false);
158165
const [showDeleteEdgeAlert, setShowDeleteEdgeAlert] = useState(false);
159166
const [showDeleteRunsAlert, setShowDeleteRunsAlert] = useState(false);
160167
const [showIntegrationsDialog, setShowIntegrationsDialog] = useState(false);
161168
const [isRefreshing, setIsRefreshing] = useState(false);
162169
const [activeTab, setActiveTab] = useAtom(propertiesPanelActiveTabAtom);
163170
const refreshRunsRef = useRef<(() => Promise<void>) | null>(null);
171+
const autoSelectAbortControllersRef = useRef<Record<string, AbortController>>(
172+
{}
173+
);
164174
const selectedNode = nodes.find((node) => node.id === selectedNodeId);
165175
const selectedEdge = edges.find((edge) => edge.id === selectedEdgeId);
166176

@@ -252,11 +262,99 @@ export const PanelInner = () => {
252262
updateNodeData({ id: selectedNode.id, data: { description } });
253263
}
254264
};
265+
const autoSelectIntegration = useCallback(
266+
async (
267+
nodeId: string,
268+
actionType: string,
269+
currentConfig: Record<string, unknown>,
270+
abortSignal: AbortSignal
271+
) => {
272+
// Get integration type - check plugin registry first, then system actions
273+
const action = findActionById(actionType);
274+
const integrationType: IntegrationType | undefined =
275+
(action?.integration as IntegrationType | undefined) ||
276+
SYSTEM_ACTION_INTEGRATIONS[actionType];
277+
278+
if (!integrationType) {
279+
// No integration needed, remove from pending
280+
setPendingIntegrationNodes((prev: Set<string>) => {
281+
const next = new Set(prev);
282+
next.delete(nodeId);
283+
return next;
284+
});
285+
return;
286+
}
287+
288+
try {
289+
const all = await api.integration.getAll();
290+
291+
// Check if this operation was aborted (actionType changed)
292+
if (abortSignal.aborted) {
293+
return;
294+
}
295+
296+
const filtered = all.filter((i) => i.type === integrationType);
297+
298+
// Auto-select if only one integration exists
299+
if (filtered.length === 1 && !abortSignal.aborted) {
300+
const newConfig = {
301+
...currentConfig,
302+
actionType,
303+
integrationId: filtered[0].id,
304+
};
305+
updateNodeData({ id: nodeId, data: { config: newConfig } });
306+
}
307+
} catch (error) {
308+
console.error("Failed to auto-select integration:", error);
309+
} finally {
310+
// Always remove from pending set when done (unless aborted)
311+
if (!abortSignal.aborted) {
312+
setPendingIntegrationNodes((prev: Set<string>) => {
313+
const next = new Set(prev);
314+
next.delete(nodeId);
315+
return next;
316+
});
317+
}
318+
}
319+
},
320+
[updateNodeData, setPendingIntegrationNodes]
321+
);
255322

256323
const handleUpdateConfig = (key: string, value: string) => {
257324
if (selectedNode) {
258-
const newConfig = { ...selectedNode.data.config, [key]: value };
325+
let newConfig = { ...selectedNode.data.config, [key]: value };
326+
327+
// When action type changes, clear the integrationId since it may not be valid for the new action
328+
if (key === "actionType" && selectedNode.data.config?.integrationId) {
329+
newConfig = { ...newConfig, integrationId: undefined };
330+
}
331+
259332
updateNodeData({ id: selectedNode.id, data: { config: newConfig } });
333+
334+
// When action type changes, auto-select integration if only one exists
335+
if (key === "actionType") {
336+
// Cancel any pending auto-select operation for this node
337+
const existingController =
338+
autoSelectAbortControllersRef.current[selectedNode.id];
339+
if (existingController) {
340+
existingController.abort();
341+
}
342+
343+
// Create new AbortController for this operation
344+
const newController = new AbortController();
345+
autoSelectAbortControllersRef.current[selectedNode.id] = newController;
346+
347+
// Add to pending set before starting async check
348+
setPendingIntegrationNodes((prev: Set<string>) =>
349+
new Set(prev).add(selectedNode.id)
350+
);
351+
autoSelectIntegration(
352+
selectedNode.id,
353+
value,
354+
newConfig,
355+
newController.signal
356+
);
357+
}
260358
}
261359
};
262360

components/workflow/nodes/action-node.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { IntegrationIcon } from "@/components/ui/integration-icon";
2424
import { cn } from "@/lib/utils";
2525
import {
2626
executionLogsAtom,
27+
pendingIntegrationNodesAtom,
2728
selectedExecutionIdAtom,
2829
type WorkflowNodeData,
2930
} from "@/lib/workflow-store";
@@ -245,6 +246,7 @@ type ActionNodeProps = NodeProps & {
245246
export const ActionNode = memo(({ data, selected, id }: ActionNodeProps) => {
246247
const selectedExecutionId = useAtomValue(selectedExecutionIdAtom);
247248
const executionLogs = useAtomValue(executionLogsAtom);
249+
const pendingIntegrationNodes = useAtomValue(pendingIntegrationNodesAtom);
248250

249251
if (!data) {
250252
return null;
@@ -299,8 +301,12 @@ export const ActionNode = memo(({ data, selected, id }: ActionNodeProps) => {
299301
data.description || getIntegrationFromActionType(actionType);
300302

301303
const needsIntegration = requiresIntegration(actionType);
304+
// Don't show missing indicator if we're still checking for auto-select
305+
const isPendingIntegrationCheck = pendingIntegrationNodes.has(id);
302306
const integrationMissing =
303-
needsIntegration && !hasIntegrationConfigured(data.config || {});
307+
needsIntegration &&
308+
!hasIntegrationConfigured(data.config || {}) &&
309+
!isPendingIntegrationCheck;
304310

305311
// Get model for AI nodes
306312
const getAiModel = (): string | null => {

lib/workflow-store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export const hasSidebarBeenShownAtom = atom<boolean>(false);
3939
export const isSidebarCollapsedAtom = atom<boolean>(false);
4040
export const isTransitioningFromHomepageAtom = atom<boolean>(false);
4141

42+
// Tracks nodes that are pending integration auto-select check
43+
// Don't show "missing integration" warning for these nodes
44+
export const pendingIntegrationNodesAtom = atom<Set<string>>(new Set<string>());
45+
4246
// Execution log entry type for storing run outputs per node
4347
export type ExecutionLogEntry = {
4448
nodeId: string;

0 commit comments

Comments
 (0)