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
38 changes: 37 additions & 1 deletion apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1292,10 +1292,43 @@ export class PostHogAPIClient {
async getGithubRepositories(
integrationId: string | number,
): Promise<string[]> {
const repositories: string[] = [];
let offset = 0;

while (true) {
const page = await this.getGithubRepositoriesPage(
integrationId,
offset,
500,
);
repositories.push(...page.repositories);

if (!page.hasMore) {
return repositories;
}

offset += page.repositories.length;
}
}

async getGithubRepositoriesPage(
integrationId: string | number,
offset: number,
limit: number,
search?: string,
): Promise<{
repositories: string[];
hasMore: boolean;
}> {
const teamId = await this.getTeamId();
const url = new URL(
`${this.api.baseUrl}/api/environments/${teamId}/integrations/${integrationId}/github_repos/`,
);
url.searchParams.set("offset", String(offset));
url.searchParams.set("limit", String(limit));
if (search?.trim()) {
url.searchParams.set("search", search.trim());
}
const response = await this.api.fetcher.fetch({
method: "get",
url,
Expand All @@ -1309,7 +1342,10 @@ export class PostHogAPIClient {
}

const data = await response.json();
return this.normalizeGithubRepositories(data);
return {
repositories: this.normalizeGithubRepositories(data),
hasMore: data.has_more ?? false,
};
}

async refreshGithubRepositories(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxListFooter,
ComboboxTrigger,
} from "@posthog/quill";
import { type RefObject, useEffect, useRef, useState } from "react";
import { defaultFilter } from "cmdk";
import { type RefObject, useEffect, useMemo, useRef, useState } from "react";

const COMBOBOX_LIMIT = 50;
const COMBOBOX_INITIAL_LIMIT = 50;

interface GitHubRepoPickerProps {
value: string | null;
Expand All @@ -27,6 +29,12 @@ interface GitHubRepoPickerProps {
showSearchInput?: boolean;
onRefresh?: () => void;
isRefreshing?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
searchQuery?: string;
onSearchQueryChange?: (value: string) => void;
hasMore?: boolean;
onLoadMore?: () => void;
}

export function GitHubRepoPicker({
Expand All @@ -40,18 +48,47 @@ export function GitHubRepoPicker({
showSearchInput = true,
onRefresh,
isRefreshing = false,
open: controlledOpen,
onOpenChange,
searchQuery: controlledSearchQuery,
onSearchQueryChange,
hasMore: controlledHasMore,
onLoadMore,
}: GitHubRepoPickerProps) {
const triggerRef = useRef<HTMLButtonElement>(null);
const [searchQuery, setSearchQuery] = useState("");
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const [uncontrolledSearchQuery, setUncontrolledSearchQuery] = useState("");
const [visibleLimit, setVisibleLimit] = useState(COMBOBOX_INITIAL_LIMIT);
const open = controlledOpen ?? uncontrolledOpen;
const searchQuery = controlledSearchQuery ?? uncontrolledSearchQuery;
const remoteMode =
controlledSearchQuery !== undefined ||
onSearchQueryChange !== undefined ||
controlledHasMore !== undefined ||
onLoadMore !== undefined;
const showInlineLoadingState = remoteMode && open && isLoading;
const onlyRepo = repositories.length === 1 ? repositories[0] : null;
const trimmedSearchQuery = searchQuery.trim();
const filteredRepositoryCount = useMemo(() => {
if (!trimmedSearchQuery) {
return repositories.length;
}

return repositories.reduce(
(count, repo) =>
count + (defaultFilter(repo, trimmedSearchQuery) > 0 ? 1 : 0),
0,
);
}, [repositories, trimmedSearchQuery]);
const hasMore = controlledHasMore ?? filteredRepositoryCount > visibleLimit;

useEffect(() => {
if (onlyRepo && value !== onlyRepo) {
onChange(onlyRepo);
}
}, [onlyRepo, value, onChange]);

if (isLoading) {
if (isLoading && !showInlineLoadingState) {
return (
<Button variant="outline" disabled size="sm">
<GithubLogo size={16} weight="regular" style={{ flexShrink: 0 }} />
Expand All @@ -60,7 +97,7 @@ export function GitHubRepoPicker({
);
}

if (repositories.length === 0) {
if (repositories.length === 0 && !showInlineLoadingState) {
return (
<Button variant="outline" disabled size="sm">
<GithubLogo size={16} weight="regular" style={{ flexShrink: 0 }} />
Expand Down Expand Up @@ -92,13 +129,27 @@ export function GitHubRepoPicker({
return (
<Combobox
items={repositories}
limit={COMBOBOX_LIMIT}
limit={visibleLimit}
value={value}
onValueChange={(v) => {
onChange(v ? (v as string) : null);
}}
open={open}
onOpenChange={(nextOpen) => {
setUncontrolledOpen(nextOpen);
onOpenChange?.(nextOpen);
if (!nextOpen) {
setUncontrolledSearchQuery("");
onSearchQueryChange?.("");
setVisibleLimit(COMBOBOX_INITIAL_LIMIT);
}
}}
inputValue={searchQuery}
onInputValueChange={setSearchQuery}
onInputValueChange={(nextSearchQuery) => {
setUncontrolledSearchQuery(nextSearchQuery);
onSearchQueryChange?.(nextSearchQuery);
setVisibleLimit(COMBOBOX_INITIAL_LIMIT);
}}
disabled={disabled}
>
<ComboboxTrigger
Expand Down Expand Up @@ -150,7 +201,11 @@ export function GitHubRepoPicker({
) : null}
</div>
) : null}
<ComboboxEmpty>No repositories found.</ComboboxEmpty>
<ComboboxEmpty>
{showInlineLoadingState
? "Loading repositories..."
: "No repositories found."}
</ComboboxEmpty>
<ComboboxList>
{(repo: string) => (
<ComboboxItem key={repo} value={repo}>
Expand All @@ -159,12 +214,48 @@ export function GitHubRepoPicker({
)}
</ComboboxList>

{repositories.length > COMBOBOX_LIMIT && (
<div className="px-2 py-1.5 text-center text-muted-foreground text-xs">
{searchQuery
? `Showing up to ${COMBOBOX_LIMIT} matches — refine your search`
: `Showing ${COMBOBOX_LIMIT} of ${repositories.length} — type to filter`}
</div>
{(hasMore ||
(remoteMode
? repositories.length > COMBOBOX_INITIAL_LIMIT
: filteredRepositoryCount > COMBOBOX_INITIAL_LIMIT)) && (
<ComboboxListFooter>
<div className="px-2 pb-2">
<div className="px-1 pb-2 text-center text-muted-foreground text-xs">
{remoteMode
? trimmedSearchQuery
? `Showing ${repositories.length}${hasMore ? "+" : ""} matches`
: `Showing ${repositories.length}${hasMore ? "+" : ""} repositories`
: trimmedSearchQuery
? `Showing ${Math.min(visibleLimit, filteredRepositoryCount)} of ${filteredRepositoryCount} matches`
: `Showing ${Math.min(visibleLimit, repositories.length)} of ${repositories.length}`}
</div>
{hasMore ? (
<Button
variant="outline"
size="sm"
className="w-full justify-center"
onMouseDown={(event) => {
event.preventDefault();
event.stopPropagation();
}}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
if (remoteMode) {
onLoadMore?.();
return;
}

setVisibleLimit(
(currentLimit) => currentLimit + COMBOBOX_INITIAL_LIMIT,
);
}}
>
Load more
</Button>
) : null}
</div>
</ComboboxListFooter>
)}
</ComboboxContent>
</Combobox>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +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 { useRepositoryIntegration } from "@hooks/useIntegrations";
import {
useGithubRepositories,
useRepositoryIntegration,
} from "@hooks/useIntegrations";
import { Box, Button, Flex, Text, TextField } from "@radix-ui/themes";
import { trpcClient } from "@renderer/trpc";
import { useCallback, useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -67,6 +70,14 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
refreshRepositories,
hasGithubIntegration,
} = useRepositoryIntegration();
const [repoPickerSearchQuery, setRepoPickerSearchQuery] = useState("");
const [isRepoPickerOpen, setIsRepoPickerOpen] = useState(false);
const {
repositories: visibleRepositories,
isPending: visibleRepositoriesLoading,
hasMore: visibleRepositoriesHasMore,
loadMore: loadMoreVisibleRepositories,
} = useGithubRepositories(repoPickerSearchQuery, isRepoPickerOpen);
const [repo, setRepo] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [connecting, setConnecting] = useState(false);
Expand Down Expand Up @@ -194,6 +205,21 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
});
}, [refreshRepositories]);

const handleRepoPickerOpenChange = useCallback((open: boolean) => {
setIsRepoPickerOpen(open);
if (!open) {
setRepoPickerSearchQuery("");
}
}, []);

const handleRepoPickerSearchChange = useCallback((value: string) => {
setRepoPickerSearchQuery(value);
}, []);

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

if (!hasGithubIntegration) {
return (
<SetupFormContainer title="Connect GitHub">
Expand Down Expand Up @@ -229,10 +255,18 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
<GitHubRepoPicker
value={repo}
onChange={setRepo}
repositories={repositories}
isLoading={isLoadingRepos}
repositories={isRepoPickerOpen ? visibleRepositories : repositories}
isLoading={
isLoadingRepos || (isRepoPickerOpen && visibleRepositoriesLoading)
}
isRefreshing={isRefreshingRepos}
onRefresh={handleRefreshRepositories}
open={isRepoPickerOpen}
onOpenChange={handleRepoPickerOpenChange}
searchQuery={repoPickerSearchQuery}
onSearchQueryChange={handleRepoPickerSearchChange}
hasMore={visibleRepositoriesHasMore}
onLoadMore={handleLoadMoreRepositories}
placeholder="Select repository..."
size="2"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping";
import { useConnectivity } from "@hooks/useConnectivity";
import {
useGithubBranches,
useGithubRepositories,
useRepositoryIntegration,
} from "@hooks/useIntegrations";
import { ButtonGroup } from "@posthog/quill";
Expand Down Expand Up @@ -79,6 +80,8 @@ export function TaskInput({
const [isDraggingFile, setIsDraggingFile] = useState(false);
const [isCreatingBranch, setIsCreatingBranch] = useState(false);
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
const [cloudRepoSearchQuery, setCloudRepoSearchQuery] = useState("");
const [isCloudRepoPickerOpen, setIsCloudRepoPickerOpen] = useState(false);
const [cloudBranchSearchQuery, setCloudBranchSearchQuery] = useState("");
const [isCloudBranchPickerOpen, setIsCloudBranchPickerOpen] = useState(false);
const [selectedEnvironment, setSelectedEnvironmentRaw] = useState<
Expand Down Expand Up @@ -114,6 +117,12 @@ export function TaskInput({
isRefreshingRepos,
refreshRepositories,
} = useRepositoryIntegration();
const {
repositories: visibleCloudRepositories,
isPending: cloudRepositoriesLoading,
hasMore: cloudRepositoriesHasMore,
loadMore: loadMoreCloudRepositories,
} = useGithubRepositories(cloudRepoSearchQuery, isCloudRepoPickerOpen);
const [selectedRepository, setSelectedRepository] = useState<string | null>(
() => lastUsedCloudRepository?.toLowerCase() ?? null,
);
Expand Down Expand Up @@ -219,6 +228,21 @@ export function TaskInput({
setIsCloudBranchPickerOpen(true);
}, []);

const handleCloudRepoPickerOpenChange = useCallback((open: boolean) => {
setIsCloudRepoPickerOpen(open);
if (!open) {
setCloudRepoSearchQuery("");
}
}, []);

const handleCloudRepoSearchChange = useCallback((value: string) => {
setCloudRepoSearchQuery(value);
}, []);

const handleLoadMoreCloudRepositories = useCallback(() => {
loadMoreCloudRepositories();
}, [loadMoreCloudRepositories]);

const handleCloudBranchPickerClose = useCallback(() => {
setIsCloudBranchPickerOpen(false);
setCloudBranchSearchQuery("");
Expand Down Expand Up @@ -521,10 +545,23 @@ export function TaskInput({
<GitHubRepoPicker
value={selectedRepository}
onChange={handleRepositorySelect}
repositories={repositories}
isLoading={isLoadingRepos}
repositories={
isCloudRepoPickerOpen
? visibleCloudRepositories
: repositories
}
isLoading={
isLoadingRepos ||
(isCloudRepoPickerOpen && cloudRepositoriesLoading)
}
Comment thread
tatoalo marked this conversation as resolved.
isRefreshing={isRefreshingRepos}
onRefresh={handleRefreshRepositories}
open={isCloudRepoPickerOpen}
onOpenChange={handleCloudRepoPickerOpenChange}
searchQuery={cloudRepoSearchQuery}
onSearchQueryChange={handleCloudRepoSearchChange}
hasMore={cloudRepositoriesHasMore}
onLoadMore={handleLoadMoreCloudRepositories}
placeholder="Select repository..."
size="1"
disabled={isCreatingTask}
Expand Down
Loading
Loading