mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 12:19:48 +02:00
[eric] onboarding: fix Connect doing nothing (extract shared useConnectIntegration: create->oauth->named popup->poll status; reused downward)
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
// D4: optional connect. Reuses the REAL shared connector catalog (icons + metadata), the REAL
|
||||
// connection state from toolsSlice, and the REAL create+OAuth thunks. All connectable MCPs, scrollable.
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { INTEGRATIONS, Integration } from '@/shared/integrations/catalog';
|
||||
import { fetchTools, createTool, startOAuth, startDeviceCodeLogin, ToolDefinition } from '@/shared/state/toolsSlice';
|
||||
import { useConnectIntegration } from '@/shared/integrations/useConnectIntegration';
|
||||
import { fetchTools, ToolDefinition } from '@/shared/state/toolsSlice';
|
||||
import { useOnboardingSkin } from '../onboardingSkin';
|
||||
import { Heading, Sub, PrimaryButton, GhostLink } from '../OnboardingAtoms';
|
||||
|
||||
@@ -14,30 +15,17 @@ function toolFor(integration: Integration, items: Record<string, ToolDefinition>
|
||||
|
||||
const Row: React.FC<{ integration: Integration }> = ({ integration }) => {
|
||||
const S = useOnboardingSkin();
|
||||
const dispatch = useAppDispatch();
|
||||
const connectIntegration = useConnectIntegration();
|
||||
const items = useAppSelector((s) => s.tools.items);
|
||||
const tool = toolFor(integration, items);
|
||||
const connected = tool?.auth_status === 'connected';
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
// The tool has to exist before OAuth (fresh installs have none) -> create from the catalog first.
|
||||
let t = tool;
|
||||
if (!t) {
|
||||
t = await dispatch(createTool({
|
||||
name: integration.name,
|
||||
description: integration.description,
|
||||
mcp_config: integration.mcp_config,
|
||||
auth_type: integration.authType ?? 'oauth2',
|
||||
})).unwrap();
|
||||
}
|
||||
if (integration.authType === 'device_code') {
|
||||
await dispatch(startDeviceCodeLogin(t.id));
|
||||
} else {
|
||||
const res = await dispatch(startOAuth(t.id)).unwrap();
|
||||
if (res.auth_url) window.open(res.auth_url, '_blank');
|
||||
}
|
||||
} catch { /* connect failed; leave as-is */ }
|
||||
if (connecting) return;
|
||||
setConnecting(true);
|
||||
await connectIntegration(integration, tool);
|
||||
setConnecting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -50,7 +38,7 @@ const Row: React.FC<{ integration: Integration }> = ({ integration }) => {
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
onClick={connected ? undefined : connect}
|
||||
onClick={connected || connecting ? undefined : connect}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
flexShrink: 0,
|
||||
@@ -60,11 +48,12 @@ const Row: React.FC<{ integration: Integration }> = ({ integration }) => {
|
||||
border: `1px solid ${connected ? S.border : S.borderStrong}`,
|
||||
borderRadius: 999,
|
||||
padding: '6px 15px',
|
||||
cursor: connected ? 'default' : 'pointer',
|
||||
cursor: connected || connecting ? 'default' : 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: connecting ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{connected ? 'Connected' : 'Connect'}
|
||||
{connected ? 'Connected' : connecting ? 'Connecting…' : 'Connect'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Shared connect primitive: create the tool (if needed) -> start OAuth / device-code -> open the
|
||||
// popup -> poll status until connected. Lives in shared/ so both the Tools page and Onboarding use
|
||||
// the SAME proven flow (downward abstraction), instead of each re-implementing it.
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { createTool, startOAuth, startDeviceCodeLogin, fetchToolStatus, ToolDefinition } from '@/shared/state/toolsSlice';
|
||||
import { Integration } from './catalog';
|
||||
|
||||
const POLL_MS = 2000;
|
||||
const POLL_MAX = 60; // ~2 min
|
||||
|
||||
export interface ConnectResult {
|
||||
status: 'connected' | 'cancelled' | 'error';
|
||||
}
|
||||
|
||||
export function useConnectIntegration(): (integration: Integration, existing?: ToolDefinition) => Promise<ConnectResult> {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return useCallback(async (integration: Integration, existing?: ToolDefinition): Promise<ConnectResult> => {
|
||||
try {
|
||||
// The tool must exist before OAuth (fresh installs have none) -> create from the catalog.
|
||||
let tool = existing;
|
||||
if (!tool) {
|
||||
tool = await dispatch(createTool({
|
||||
name: integration.name,
|
||||
description: integration.description,
|
||||
mcp_config: integration.mcp_config,
|
||||
auth_type: integration.authType ?? 'oauth2',
|
||||
})).unwrap();
|
||||
}
|
||||
|
||||
if (integration.authType === 'device_code') {
|
||||
await dispatch(startDeviceCodeLogin(tool.id));
|
||||
return { status: 'connected' };
|
||||
}
|
||||
|
||||
const { auth_url } = await dispatch(startOAuth(tool.id)).unwrap();
|
||||
if (!auth_url) return { status: 'error' };
|
||||
// Named popup + features = the pattern that actually opens in Electron (a bare _blank is swallowed).
|
||||
window.open(auth_url, 'oauth', 'width=500,height=700,left=200,top=100');
|
||||
|
||||
// Poll until the OAuth round-trip flips auth_status to connected.
|
||||
for (let i = 0; i < POLL_MAX; i++) {
|
||||
await new Promise((r) => setTimeout(r, POLL_MS));
|
||||
const t = await dispatch(fetchToolStatus(tool.id)).unwrap();
|
||||
if (t.auth_status === 'connected') return { status: 'connected' };
|
||||
}
|
||||
return { status: 'cancelled' };
|
||||
} catch {
|
||||
return { status: 'error' };
|
||||
}
|
||||
}, [dispatch]);
|
||||
}
|
||||
Reference in New Issue
Block a user