Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion packages/react/src/pages/integration-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export function IntegrationDetailPage(props: { namespace: string }) {
const [refreshing, setRefreshing] = useState(false);
const [editSheetOpen, setEditSheetOpen] = useState(false);
const [activeTab, setActiveTab] = useState<"accounts" | "tools">("accounts");
const [manualAccountHandoff, setManualAccountHandoff] =
useState<IntegrationAccountHandoff | null>(null);
const [locationSearch] = useState(() =>
typeof window === "undefined" ? "" : window.location.search,
);
Expand All @@ -105,7 +107,7 @@ export function IntegrationDetailPage(props: { namespace: string }) {
const currentTab = isBuiltInIntegration ? "tools" : activeTab;
const canRefresh = integrationData?.canRefresh ?? false;
const canRemove = integrationData?.canRemove ?? false;
const accountHandoff = useMemo<IntegrationAccountHandoff | null>(() => {
const urlAccountHandoff = useMemo<IntegrationAccountHandoff | null>(() => {
if (locationSearch.length === 0) return null;
const search = new URLSearchParams(locationSearch);
if (search.get("addAccount") !== "1") return null;
Expand Down Expand Up @@ -140,6 +142,8 @@ export function IntegrationDetailPage(props: { namespace: string }) {
...(oauthClient !== undefined ? { oauthClient } : {}),
};
}, [locationSearch]);
const accountHandoff = manualAccountHandoff ?? urlAccountHandoff;

useEffect(() => {
if (accountHandoff && !isBuiltInIntegration) {
setActiveTab("accounts");
Expand Down Expand Up @@ -338,6 +342,11 @@ export function IntegrationDetailPage(props: { namespace: string }) {
setRefreshing(false);
};

const handleOpenAddConnection = () => {
setActiveTab("accounts");
setManualAccountHandoff({ key: `manual:${String(slug)}:${Date.now()}` });
};
Comment on lines +345 to +348

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale manualAccountHandoff can re-open the dialog after dismissal

handleOpenAddConnection sets manualAccountHandoff with a Date.now() key, but it is never reset to null after the user dismisses the dialog. AccountsSection calls setAdding(true) inside a useEffect([accountHandoff]), so any future re-mount of the Accounts panel (e.g. due to a parent re-render that drops and recreates the TabsContent subtree) would fire that effect again with the stale handoff and reopen the modal unexpectedly. Clearing manualAccountHandoff when the user leaves the Accounts dialog — or at minimum after the tab switch completes — would prevent this.


return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
{/* Header bar */}
Expand Down Expand Up @@ -491,6 +500,11 @@ export function IntegrationDetailPage(props: { namespace: string }) {
}
: {})}
/>
) : !isBuiltInIntegration && integrationConnections.length === 0 ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Race condition: "No tools yet" flashes for users who have connections

integrationConnections falls back to [] whenever connectionsResult is not yet Success (line 219). If the tools atom resolves first — a common split-fetch scenario — the NoConnectionToolsEmptyState is momentarily rendered even for integrations with active connections. During that window the right panel shows "No tools yet / Add a connection" while the left ToolTree already displays the loaded tools, producing a confusing split-state flash. The guard needs to confirm that connectionsResult has actually loaded before trusting an empty integrationConnections.

Suggested change
) : !isBuiltInIntegration && integrationConnections.length === 0 ? (
) : !isBuiltInIntegration && AsyncResult.isSuccess(connectionsResult) && integrationConnections.length === 0 ? (

<NoConnectionToolsEmptyState
onAddConnection={handleOpenAddConnection}
canAddConnection={accountsMethods.length > 0}
/>
) : (
<ToolDetailEmpty hasTools={integrationTools.length > 0} />
)}
Expand All @@ -514,6 +528,31 @@ export function IntegrationDetailPage(props: { namespace: string }) {
);
}

function NoConnectionToolsEmptyState(props: {
readonly onAddConnection: () => void;
readonly canAddConnection: boolean;
}) {
return (
<div className="flex h-full items-center justify-center">
<div className="max-w-sm text-center">
<p className="text-sm font-medium text-foreground">No tools yet</p>
<p className="mt-1.5 text-sm text-muted-foreground">
Add a connection to unlock this integration's tools.
</p>
<Button
type="button"
size="sm"
className="mt-4"
onClick={props.onAddConnection}
disabled={!props.canAddConnection}
>
Add connection
</Button>
</div>
</div>
);
}

function IntegrationDetailSkeleton() {
return (
<div className="flex min-h-0 flex-1 overflow-hidden">
Expand Down
Loading