Skip to content
Merged
Show file tree
Hide file tree
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
116 changes: 33 additions & 83 deletions apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { GitHubRepoPicker } from "@features/folder-picker/components/GitHubRepoPicker";
import {
describeGithubConnectError,
useGithubUserConnect,
} from "@features/integrations/hooks/useGithubUserConnect";
import {
useGithubRepositories,
useRepositoryIntegration,
Expand Down Expand Up @@ -55,12 +59,8 @@ interface SetupFormProps {
onCancel: () => void;
}

const POLL_INTERVAL_GITHUB_MS = 3_000;
const POLL_TIMEOUT_GITHUB_MS = 300_000;

function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
const projectId = useAuthStateValue((state) => state.projectId);
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
const client = useAuthenticatedClient();
const {
repositories,
Expand All @@ -80,26 +80,17 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
} = useGithubRepositories(repoPickerSearchQuery, isRepoPickerOpen);
const [repo, setRepo] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [connecting, setConnecting] = useState(false);
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const {
error: connectError,
isConnecting: connecting,
isTimedOut: timedOut,
hasError: hasConnectError,
connect: handleConnectGitHub,
} = useGithubUserConnect({ projectId });
const selectedIntegrationId = repo
? getIntegrationIdForRepo(repo)
: undefined;

const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
if (pollTimeoutRef.current) {
clearTimeout(pollTimeoutRef.current);
pollTimeoutRef.current = null;
}
}, []);

useEffect(() => stopPolling, [stopPolling]);

useEffect(() => {
if (isLoadingRepos || !repo || repositories.includes(repo)) {
return;
Expand All @@ -108,62 +99,13 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
setRepo(null);
}, [isLoadingRepos, repo, repositories]);

// Stop polling once integration appears
useEffect(() => {
if (hasGithubIntegration && connecting) {
stopPolling();
setConnecting(false);
}
}, [hasGithubIntegration, connecting, stopPolling]);

// Auto-select the first repo once loaded
useEffect(() => {
if (repo === null && repositories.length > 0) {
setRepo(repositories[0]);
}
}, [repo, repositories]);

const handleConnectGitHub = useCallback(async () => {
if (!cloudRegion || !projectId) return;
setConnecting(true);
try {
await trpcClient.githubIntegration.startFlow.mutate({
region: cloudRegion,
projectId,
});

pollTimerRef.current = setInterval(async () => {
try {
if (!client) return;
// Trigger a refetch of integrations
const integrations =
await client.getIntegrationsForProject(projectId);
const hasGithub = integrations.some(
(i: { kind: string }) => i.kind === "github",
);
if (hasGithub) {
stopPolling();
setConnecting(false);
toast.success("GitHub connected");
}
} catch {
// Ignore individual poll failures
}
}, POLL_INTERVAL_GITHUB_MS);

pollTimeoutRef.current = setTimeout(() => {
stopPolling();
setConnecting(false);
toast.error("Connection timed out. Please try again.");
}, POLL_TIMEOUT_GITHUB_MS);
} catch (error) {
setConnecting(false);
toast.error(
error instanceof Error ? error.message : "Failed to start GitHub flow",
);
}
}, [cloudRegion, projectId, client, stopPolling]);

const handleSubmit = useCallback(async () => {
if (!projectId || !client || !repo || !selectedIntegrationId) return;

Expand Down Expand Up @@ -216,32 +158,40 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
setRepoPickerSearchQuery(value);
}, []);

const handleLoadMoreRepositories = useCallback(() => {
loadMoreVisibleRepositories();
}, [loadMoreVisibleRepositories]);

if (!hasGithubIntegration) {
const statusMessage = hasConnectError
? describeGithubConnectError(connectError)
: timedOut
? "We didn't hear back from GitHub. If the browser tab was closed, click Try again."
: connecting
? "Waiting for GitHub… finish authorizing in your browser, then return here."
: "Connect your GitHub account to import issues as signals.";
return (
<SetupFormContainer title="Connect GitHub">
<Flex direction="column" gap="3">
<Text className="text-(--gray-11) text-sm">
Connect your GitHub account to import issues as signals.
<Text
className={
hasConnectError
? "text-(--red-11) text-sm"
: "text-(--gray-11) text-sm"
}
>
{statusMessage}
</Text>
<Flex gap="2" justify="end">
<Button
size="2"
variant="soft"
onClick={onCancel}
disabled={connecting}
>
<Button size="2" variant="soft" onClick={onCancel}>
Cancel
</Button>
<Button
size="2"
onClick={() => void handleConnectGitHub()}
disabled={connecting}
>
{connecting ? "Waiting for authorization..." : "Connect GitHub"}
{connecting
? "Waiting for authorization..."
: hasConnectError || timedOut
? "Try again"
: "Connect GitHub"}
</Button>
</Flex>
</Flex>
Expand All @@ -266,7 +216,7 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
searchQuery={repoPickerSearchQuery}
onSearchQueryChange={handleRepoPickerSearchChange}
hasMore={visibleRepositoriesHasMore}
onLoadMore={handleLoadMoreRepositories}
onLoadMore={loadMoreVisibleRepositories}
placeholder="Select repository..."
size="2"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ export function GitHubConnectionBanner() {
const projectId = useAuthStateValue((s) => s.projectId);
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);

const { state, error, connect, reset } = useGithubUserConnect({ projectId });
const connecting = state === "connecting";
const hasConnectError = state === "error";
const {
error,
isConnecting: connecting,
hasError: hasConnectError,
connect,
reset,
} = useGithubUserConnect({ projectId });
const canConnectCloud = projectId != null && cloudRegion != null;

if (loginLoading) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { useGitHubIntegrationCallback } from "@features/integrations/hooks/useGitHubIntegrationCallback";
import { trpcClient } from "@renderer/trpc/client";
import { IS_DEV } from "@shared/constants/environment";
Expand Down Expand Up @@ -64,6 +63,9 @@ interface Options {
interface Result {
state: GithubUserConnectState;
error: GithubUserConnectError | null;
isConnecting: boolean;
isTimedOut: boolean;
hasError: boolean;
connect: () => Promise<void>;
reset: () => void;
}
Expand Down Expand Up @@ -96,7 +98,6 @@ export async function openUrlInBrowser(url: string): Promise<void> {

export function useGithubUserConnect({ projectId }: Options): Result {
const client = useOptionalAuthenticatedClient();
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
const queryClient = useQueryClient();
const [state, setState] = useState<GithubUserConnectState>("idle");
const [error, setError] = useState<GithubUserConnectError | null>(null);
Expand Down Expand Up @@ -153,7 +154,7 @@ export function useGithubUserConnect({ projectId }: Options): Result {

const connect = useCallback(async () => {
if (stateRef.current === "connecting") return;
if (!cloudRegion || projectId === null || !client) return;
if (projectId === null || !client) return;
stopPolling();
setError(null);
setState("connecting");
Expand Down Expand Up @@ -184,13 +185,21 @@ export function useGithubUserConnect({ projectId }: Options): Result {
code: null,
});
}
}, [client, cloudRegion, projectId, invalidate, stopPolling]);
}, [client, projectId, invalidate, stopPolling]);

const reset = useCallback(() => {
stopPolling();
setError(null);
setState("idle");
}, [stopPolling]);

return { state, error, connect, reset };
return {
state,
error,
isConnecting: state === "connecting",
isTimedOut: state === "timed-out",
hasError: state === "error",
connect,
reset,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,13 @@ export function GitIntegrationStep({
}, [manuallySelectedProjectId, currentProjectId, projects]);

const {
state: connectState,
error: connectError,
isConnecting,
isTimedOut: timedOut,
hasError: hasConnectError,
connect: handleConnectGitHub,
reset: resetConnect,
} = useGithubUserConnect({ projectId: selectedProjectId });
const isConnecting = connectState === "connecting";
const timedOut = connectState === "timed-out";
const hasConnectError = connectState === "error";
const canTakeAction = !isConnecting && !timedOut && !hasConnectError;
const defaultPanelMessage = hasConnectError
? describeGithubConnectError(connectError)
Expand Down
Loading
Loading