fix(ui): simplify GitHub repository access controls (#13047)
## Thinking Path > - Paperclip helps people manage AI agents and their connected apps. > - GitHub permissions show which repositories an account can access. > - The page adds an account dropdown and search above the repository list. > - These controls add clutter to a view meant to show the full access list. > - This change removes both controls and their filtering state. > - People see all repositories directly, with the existing scrolling and access controls. ## Linked Issues or Issue Description Refs #12998. **Current behavior** GitHub permissions show an “All accounts” dropdown and a “Search repositories” input above the repository list. **Proposed behavior** Show the full repository list directly. Keep the scroll limit, repository links, private-repository icons, refresh button, and GitHub configuration links. **Reason and benefit** Remove unnecessary controls from the access summary. Users manage repository permissions on GitHub. ## What Changed - Remove the account dropdown, search input, filtering state, and unused imports. - Rename the GitHub configuration button to “Add More Repos on GitHub”. - Render every returned repository and simplify the empty-list message. - Update the existing multi-account test and permissions documentation. ## Verification - All 55 AppDetail tests passed. - `pnpm check:token-gates` passed. - Full `pnpm build` and `pnpm -r typecheck` passed. All 31 CI checks passed on `cb9b8bb14a880d926393c62821265fbd2d8c5d31`. Storybook visual regression was correctly skipped. - Fresh Greptile review on the same commit: 5/5, with no unresolved findings. ## Risks Users can no longer narrow this list by account or search text. The list still scrolls and shows each repository’s full owner/name. This change does not alter GitHub permissions or credential selection. ## Model Used OpenAI GPT-6 through Codex, with code editing and shell verification tools. The exact model variant and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
be6bb768b1
commit
db85bf4b7a
|
|
@ -28,6 +28,8 @@ An explicit dedicated-agent grant overrides personal selection. Revoked, disable
|
|||
|
||||
Connection setup and permissions display: “This agent uses this GitHub account for everyone's work, instead of the person giving instructions.”
|
||||
|
||||
The GitHub permissions page shows repositories across all connected accounts in one scrollable list. It has no account filter or repository search. Repository icons, private-repository indicators, refresh, and GitHub configuration links remain available. The “Add More Repos on GitHub” button opens GitHub’s app installation and repository-access setup.
|
||||
|
||||
Multiple eligible connections for the same GitHub account are treated as one
|
||||
identity, using GitHub's stable account ID rather than its login. The resolver
|
||||
selects an available grant, preferring the newest authorization with a stable
|
||||
|
|
|
|||
|
|
@ -1508,19 +1508,19 @@ describe("AppDetail", () => {
|
|||
await act(async () => { findButton("Load GitHub configuration")!.click(); });
|
||||
await flushReact();
|
||||
expect(checkConnectionHealthMock).toHaveBeenCalledWith("conn-1");
|
||||
expect(container.querySelector('a[href="https://github.com/apps/paperclip-staging/installations/new"]')?.textContent).toBe("Configure on GitHub");
|
||||
expect(container.querySelector('a[href="https://github.com/apps/paperclip-staging/installations/new"]')?.textContent).toBe("Add More Repos on GitHub");
|
||||
expect(findButton("Load GitHub configuration")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters the combined GitHub repository list by owner and search without changing access", async () => {
|
||||
it.each([false, true])("shows repositories across accounts without filter controls (empty: %s)", async (empty) => {
|
||||
mockParams.tab = "permissions";
|
||||
getConnectionMock.mockResolvedValue(perUserConnection());
|
||||
listConnectionGrantsMock.mockResolvedValue({
|
||||
connection: { id: "conn-1", uid: "conn-1" },
|
||||
grants: [dedicatedGitHubGrant({ kind: "user", subjectAgentId: null, subjectUserId: "user-1" }, {
|
||||
repositoryCount: 3,
|
||||
repositoryCount: empty ? 0 : 3,
|
||||
installationOwnerLogins: ["paperclipai", "dottabot", "empty-org"],
|
||||
repositories: [
|
||||
repositories: empty ? [] : [
|
||||
{ id: "1", fullName: "paperclipai/first", installationId: "456" },
|
||||
{ id: "2", fullName: "paperclipai/second", installationId: "456" },
|
||||
{ id: "3", fullName: "dottabot/first", installationId: "789" },
|
||||
|
|
@ -1530,31 +1530,14 @@ describe("AppDetail", () => {
|
|||
});
|
||||
await renderAppDetail();
|
||||
const repositoryNames = () => [...container.querySelectorAll('ul[aria-label="Accessible GitHub repositories"] a')].map((link) => link.textContent);
|
||||
const selectOwner = async (label: string) => {
|
||||
await act(async () => {
|
||||
container.querySelector('[role="combobox"][aria-label="Filter repositories by account or organization"]')!
|
||||
.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
});
|
||||
const option = [...document.querySelectorAll('[role="option"]')].find((item) => item.textContent === label);
|
||||
expect(option).toBeTruthy();
|
||||
await act(async () => {
|
||||
option!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
});
|
||||
};
|
||||
expect(repositoryNames()).toEqual(["paperclipai/first", "paperclipai/second", "dottabot/first"]);
|
||||
await selectOwner("paperclipai");
|
||||
expect(repositoryNames()).toEqual(["paperclipai/first", "paperclipai/second"]);
|
||||
const search = container.querySelector<HTMLInputElement>('input[aria-label="Search GitHub repositories"]')!;
|
||||
await act(async () => { setInputValue(search, "FIRST"); });
|
||||
expect(repositoryNames()).toEqual(["paperclipai/first"]);
|
||||
await selectOwner("All accounts");
|
||||
expect(repositoryNames()).toEqual(["paperclipai/first", "dottabot/first"]);
|
||||
await act(async () => { setInputValue(search, "missing"); });
|
||||
expect(container.textContent).toContain("No repositories match your search.");
|
||||
await act(async () => { setInputValue(search, ""); });
|
||||
await selectOwner("empty-org");
|
||||
expect(container.textContent).toContain("No accessible repositories for this account or organization.");
|
||||
expect(container.querySelector('a[href="https://github.com/apps/paperclip-test/installations/new"]')?.textContent).toBe("Configure on GitHub");
|
||||
expect(repositoryNames()).toEqual(empty ? [] : ["paperclipai/first", "paperclipai/second", "dottabot/first"]);
|
||||
if (empty) {
|
||||
expect(container.querySelector('p[role="status"]')?.textContent?.trim()).toBe("No accessible repositories.");
|
||||
expect(container.textContent).not.toContain("Refresh access to load the current repository list.");
|
||||
}
|
||||
expect(container.querySelector('[aria-label="Filter repositories by account or organization"]')).toBeNull();
|
||||
expect(container.querySelector('input[aria-label="Search GitHub repositories"]')).toBeNull();
|
||||
expect(container.querySelector('a[href="https://github.com/apps/paperclip-test/installations/new"]')?.textContent).toBe("Add More Repos on GitHub");
|
||||
expect(updateConnectionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -1581,7 +1564,7 @@ describe("AppDetail", () => {
|
|||
expect(container.querySelector('a[href="https://github.com/paperclipai/test-repo"]')?.textContent).toBe("paperclipai/test-repo");
|
||||
expect(container.querySelector(
|
||||
'a[href="https://github.com/apps/paperclip-test/installations/new"]',
|
||||
)?.textContent).toBe("Configure on GitHub");
|
||||
)?.textContent).toBe("Add More Repos on GitHub");
|
||||
expect(container.querySelector('button[aria-label="Refresh access"]')).toBeTruthy();
|
||||
expect(container.textContent).not.toContain("Installation");
|
||||
expect(container.textContent).not.toContain("Token continuity");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { Building2, Loader2, Lock, RefreshCw, Search, TriangleAlert, UserRound } from "lucide-react";
|
||||
import { Building2, Loader2, Lock, RefreshCw, TriangleAlert, UserRound } from "lucide-react";
|
||||
import type {
|
||||
ConnectionAudienceMember,
|
||||
ConnectionGrant,
|
||||
|
|
@ -9,8 +9,6 @@ import type {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Identity } from "@/components/Identity";
|
||||
import { GithubIcon } from "@/components/icons/github-icon";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { InlineBanner } from "@/components/InlineBanner";
|
||||
import { MemberMultiSelect } from "@/components/MemberMultiSelect";
|
||||
|
|
@ -295,24 +293,8 @@ function GitHubConnectionSummary({
|
|||
onRefreshAccess?: () => void;
|
||||
refreshPending: boolean;
|
||||
}) {
|
||||
const [repositoryOwner, setRepositoryOwner] = useState("*");
|
||||
const [repositorySearch, setRepositorySearch] = useState("");
|
||||
useEffect(() => {
|
||||
setRepositoryOwner("*");
|
||||
setRepositorySearch("");
|
||||
}, [grant.id]);
|
||||
const github = grant.providerTenant?.github;
|
||||
if (!github) return null;
|
||||
const owners = [...new Set([
|
||||
...(github.installationOwnerLogins ?? []),
|
||||
...(github.repositories ?? []).map((repository) => repository.fullName.split("/")[0]),
|
||||
])].sort((a, b) => a.localeCompare(b));
|
||||
const selectedOwner = owners.includes(repositoryOwner) ? repositoryOwner : "*";
|
||||
const search = repositorySearch.trim().toLowerCase();
|
||||
const visibleRepositories = github.repositories?.filter((repository) => (
|
||||
(selectedOwner === "*" || repository.fullName.split("/")[0] === selectedOwner)
|
||||
&& repository.fullName.toLowerCase().includes(search)
|
||||
));
|
||||
const configurationUrl = github.appSlug
|
||||
? `https://github.com/apps/${encodeURIComponent(github.appSlug)}/installations/new`
|
||||
: /^https:\/\/github\.com\/apps\/[a-z0-9-]+\/installations\/new$/.test(github.installationUrl ?? "")
|
||||
|
|
@ -361,7 +343,7 @@ function GitHubConnectionSummary({
|
|||
) : null}
|
||||
{configurationUrl ? (
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<a href={configurationUrl} target="_blank" rel="noreferrer">Configure on GitHub</a>
|
||||
<a href={configurationUrl} target="_blank" rel="noreferrer">Add More Repos on GitHub</a>
|
||||
</Button>
|
||||
) : onRefreshAccess ? (
|
||||
<Button size="sm" variant="outline" disabled={refreshPending} onClick={onRefreshAccess}>
|
||||
|
|
@ -371,27 +353,9 @@ function GitHubConnectionSummary({
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<Select value={selectedOwner} onValueChange={setRepositoryOwner}>
|
||||
<SelectTrigger aria-label="Filter repositories by account or organization" className="w-full">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<GithubIcon className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<SelectValue />
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="*">All accounts</SelectItem>
|
||||
{owners.map((owner) => <SelectItem key={owner} value={owner}>{owner}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" aria-hidden="true" />
|
||||
<Input aria-label="Search GitHub repositories" placeholder="Search repositories" className="pl-9" value={repositorySearch} onChange={(event) => setRepositorySearch(event.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
{github.repositories ? (
|
||||
visibleRepositories?.length ? <ul aria-label="Accessible GitHub repositories" tabIndex={0} className="max-h-(--sz-github-repository-list) space-y-2 overflow-y-auto text-sm">
|
||||
{visibleRepositories.map((repository) => (
|
||||
github.repositories.length ? <ul aria-label="Accessible GitHub repositories" tabIndex={0} className="max-h-(--sz-github-repository-list) space-y-2 overflow-y-auto text-sm">
|
||||
{github.repositories.map((repository) => (
|
||||
<li key={repository.id}>
|
||||
<a className="flex items-center gap-2 text-muted-foreground hover:underline" href={`https://github.com/${repository.fullName.split("/").map(encodeURIComponent).join("/")}`} target="_blank" rel="noreferrer">
|
||||
<GithubIcon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||||
|
|
@ -401,7 +365,7 @@ function GitHubConnectionSummary({
|
|||
</li>
|
||||
))}
|
||||
</ul> : <p role="status" className="text-sm text-muted-foreground">
|
||||
{search ? "No repositories match your search." : "No accessible repositories for this account or organization."}
|
||||
No accessible repositories.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Refresh access to load the current repository list.</p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue