fix(connections): simplify GitHub access details
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
5da6499860
commit
fec6dd2bf4
|
|
@ -1425,6 +1425,57 @@ describe("AppDetail", () => {
|
||||||
expect(findButton("Revoke")).toBeUndefined();
|
expect(findButton("Revoke")).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows dedicated GitHub access as compact action rows and links to the agent", async () => {
|
||||||
|
mockParams.tab = "permissions";
|
||||||
|
getConnectionMock.mockResolvedValue(connection({
|
||||||
|
credentialPolicy: "per_agent",
|
||||||
|
authKind: "oauth",
|
||||||
|
}));
|
||||||
|
listConnectionGrantsMock.mockResolvedValue({
|
||||||
|
connection: { id: "conn-1", uid: "conn-1" },
|
||||||
|
grants: [organizationGrant({
|
||||||
|
id: "grant-agent",
|
||||||
|
kind: "agent",
|
||||||
|
subjectAgentId: "agent-1",
|
||||||
|
subjectUserId: null,
|
||||||
|
isDefault: false,
|
||||||
|
providerTenant: {
|
||||||
|
github: {
|
||||||
|
userId: "123",
|
||||||
|
login: "dottabot",
|
||||||
|
installationCount: 1,
|
||||||
|
repositoryCount: 1,
|
||||||
|
repositorySelection: "selected",
|
||||||
|
installationIds: ["456"],
|
||||||
|
installationOwnerLogins: ["paperclipai"],
|
||||||
|
managementUrl: "https://github.com/settings/installations/456",
|
||||||
|
webhookHealth: "pending",
|
||||||
|
lastWebhookAt: null,
|
||||||
|
lastAccessRefreshAt: "2026-09-05T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
capabilities: fullCapabilities(),
|
||||||
|
currentUserId: "user-1",
|
||||||
|
members: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderAppDetail();
|
||||||
|
|
||||||
|
expect(container.querySelector('a[href="/agents/agent-1"]')?.textContent).toContain("Used only by Coder");
|
||||||
|
expect(container.textContent).toContain("Repositories");
|
||||||
|
expect(container.textContent).toContain("1 selected repositories");
|
||||||
|
expect(container.querySelector(
|
||||||
|
'a[href="https://github.com/settings/installations/456"]',
|
||||||
|
)?.textContent).toBe("Manage repositories on GitHub");
|
||||||
|
expect(findButton("Refresh access")).toBeTruthy();
|
||||||
|
expect(container.textContent).not.toContain("Installation");
|
||||||
|
expect(container.textContent).not.toContain("Token continuity");
|
||||||
|
expect(container.textContent).not.toContain("Webhook health");
|
||||||
|
expect(container.textContent).not.toContain("Last event");
|
||||||
|
expect(container.textContent).not.toContain("Last access refresh");
|
||||||
|
});
|
||||||
|
|
||||||
it("persists an empty audience as all organization members", async () => {
|
it("persists an empty audience as all organization members", async () => {
|
||||||
mockParams.tab = "permissions";
|
mockParams.tab = "permissions";
|
||||||
getConnectionMock.mockResolvedValue(connection({ createdByUserId: "user-1" }));
|
getConnectionMock.mockResolvedValue(connection({ createdByUserId: "user-1" }));
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,9 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { Link } from "@/lib/router";
|
||||||
import { brandChipBadge } from "@/lib/status-colors";
|
import { brandChipBadge } from "@/lib/status-colors";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
audienceUserIds,
|
audienceUserIds,
|
||||||
grantAccountLabel,
|
grantAccountLabel,
|
||||||
|
|
@ -178,7 +179,14 @@ export function IdentitiesSection({
|
||||||
<IdentityRow
|
<IdentityRow
|
||||||
title={github ? `@${github.login}` : "Dedicated GitHub account"}
|
title={github ? `@${github.login}` : "Dedicated GitHub account"}
|
||||||
status={agentGrant?.status ?? null}
|
status={agentGrant?.status ?? null}
|
||||||
detail={dedicatedAgent ? `Used only by ${dedicatedAgent.name}` : "Dedicated to one agent"}
|
detail={dedicatedAgent ? (
|
||||||
|
<Link
|
||||||
|
to={`/agents/${dedicatedAgent.id}`}
|
||||||
|
className="transition-colors hover:text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
Used only by {dedicatedAgent.name}
|
||||||
|
</Link>
|
||||||
|
) : "Dedicated to one agent"}
|
||||||
actions={!agentGrant && dedicatedAgent && capabilities?.canConfigure ? (
|
actions={!agentGrant && dedicatedAgent && capabilities?.canConfigure ? (
|
||||||
<Button size="sm" disabled={connectPending} onClick={() => onConnectAgent(dedicatedAgent.id)}>
|
<Button size="sm" disabled={connectPending} onClick={() => onConnectAgent(dedicatedAgent.id)}>
|
||||||
{connectPending ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : null}
|
{connectPending ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : null}
|
||||||
|
|
@ -288,27 +296,27 @@ function GitHubConnectionSummary({
|
||||||
}) {
|
}) {
|
||||||
const github = grant.providerTenant?.github;
|
const github = grant.providerTenant?.github;
|
||||||
if (!github) return null;
|
if (!github) return null;
|
||||||
|
const repositorySummary = github.repositorySelection === "all"
|
||||||
|
? "All repositories"
|
||||||
|
: `${github.repositoryCount} selected repositories`;
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 rounded-lg border border-border p-4">
|
<div className="divide-y divide-border border-y border-border">
|
||||||
<div className="grid gap-3 text-sm text-muted-foreground sm:grid-cols-2">
|
<div className="flex flex-wrap items-center justify-between gap-3 py-3">
|
||||||
<p><span className="font-medium text-foreground">Installation</span><br />{github.installationOwnerLogins.join(", ") || "GitHub"}</p>
|
<div className="min-w-0">
|
||||||
<p><span className="font-medium text-foreground">Repositories</span><br />{github.repositoryCount} · {github.repositorySelection === "all" ? "All repositories" : "Selected repositories"}</p>
|
<div className="text-sm font-medium text-foreground">Repositories</div>
|
||||||
<p><span className="font-medium text-foreground">Token continuity</span><br />{grant.providerTenant?.oauth?.accessTokenExpiresAt ? "Automatically refreshed" : "Long-lived"}</p>
|
<div className="text-xs text-muted-foreground">{repositorySummary}</div>
|
||||||
<p><span className="font-medium text-foreground">Webhook health</span><br />{github.webhookHealth === "healthy" ? "Healthy" : github.webhookHealth === "unhealthy" ? "Needs attention" : "Pending first event"}</p>
|
</div>
|
||||||
<p><span className="font-medium text-foreground">Last event</span><br />{github.lastWebhookAt ? new Date(github.lastWebhookAt).toLocaleString() : "No event received yet"}</p>
|
|
||||||
<p><span className="font-medium text-foreground">Last access refresh</span><br />{github.lastAccessRefreshAt ? new Date(github.lastAccessRefreshAt).toLocaleString() : "Not refreshed yet"}</p>
|
|
||||||
</div>
|
|
||||||
{github.repositorySelection === "all" ? (
|
|
||||||
<InlineBanner tone="warning" compact>
|
|
||||||
This installation can access every current and future repository in its GitHub account. Selected repositories is the safer default.
|
|
||||||
</InlineBanner>
|
|
||||||
) : null}
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{github.managementUrl ? (
|
{github.managementUrl ? (
|
||||||
<Button asChild size="sm" variant="outline">
|
<Button asChild size="sm" variant="outline">
|
||||||
<a href={github.managementUrl} target="_blank" rel="noreferrer">Manage repositories on GitHub</a>
|
<a href={github.managementUrl} target="_blank" rel="noreferrer">Manage repositories on GitHub</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3 py-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-medium text-foreground">Refresh access</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Sync repository access from GitHub.</div>
|
||||||
|
</div>
|
||||||
{onRefreshAccess ? (
|
{onRefreshAccess ? (
|
||||||
<Button size="sm" variant="outline" disabled={refreshPending} onClick={onRefreshAccess}>
|
<Button size="sm" variant="outline" disabled={refreshPending} onClick={onRefreshAccess}>
|
||||||
{refreshPending ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : null}
|
{refreshPending ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : null}
|
||||||
|
|
@ -398,7 +406,7 @@ function IdentityRow({
|
||||||
id?: string;
|
id?: string;
|
||||||
title: string;
|
title: string;
|
||||||
status: ConnectionGrant["status"] | null;
|
status: ConnectionGrant["status"] | null;
|
||||||
detail: string | null;
|
detail: ReactNode;
|
||||||
actions: ReactNode;
|
actions: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue