mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
Show clear GitHub connection recovery guidance and reconnect the saved App before requesting replacement credentials. Preserve PEM bytes through paste, upload, and retry, with production-page Storybook and browser regression coverage. Co-Authored-By: Paperclip <noreply@paperclip.ing>
663 lines
25 KiB
TypeScript
663 lines
25 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { ExternalLink, Loader2 } from "lucide-react";
|
|
import type { GitHubAppWizardState } from "@paperclipai/shared";
|
|
import { agentsApi } from "@/api/agents";
|
|
import { chatEndpointsApi } from "@/api/chatEndpoints";
|
|
import { githubChatApi } from "@/api/githubChat";
|
|
import { projectsApi } from "@/api/projects";
|
|
import { toolsApi } from "@/api/tools";
|
|
import { AgentSelect } from "@/components/AgentMultiSelect";
|
|
import { GitHubAgentTrustWarning } from "@/components/GitHubAgentTrustWarning";
|
|
import {
|
|
SetupWizardNavigation,
|
|
SetupWizardFooter,
|
|
} from "@/components/SetupWizard";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { useCompany } from "@/context/CompanyContext";
|
|
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
|
|
import { useNavigate, useSearchParams, Link } from "@/lib/router";
|
|
import { buildPermissionsForTrustPreset } from "@/lib/trust-policy-ui";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import { GitHubAppRecovery } from "./GitHubConnectionRecovery";
|
|
import type { GitHubConnectionCompleteLocationState } from "./GitHubConnectionComplete";
|
|
|
|
/** Restrict native manifest submission to GitHub registration endpoints. */
|
|
export function gitHubAppManifestAction(
|
|
registration: NonNullable<GitHubAppWizardState["registration"]>,
|
|
) {
|
|
const url = new URL(registration.registrationUrl);
|
|
if (
|
|
url.origin !== "https://github.com" ||
|
|
url.username || url.password || url.hash ||
|
|
!/^\/(settings|organizations\/[A-Za-z0-9-]+\/settings)\/apps\/new$/.test(
|
|
url.pathname,
|
|
)
|
|
)
|
|
throw new Error("GitHub returned an invalid registration address");
|
|
return url.toString();
|
|
}
|
|
function GitHubAppManifestForm({
|
|
registration, onSaveExit, disabled, autoSubmit, onSubmit,
|
|
}: {
|
|
registration: NonNullable<GitHubAppWizardState["registration"]>;
|
|
onSaveExit: () => void;
|
|
disabled: boolean;
|
|
autoSubmit: boolean;
|
|
onSubmit: () => void;
|
|
}) {
|
|
const form = useRef<HTMLFormElement>(null);
|
|
const requested = useRef(false);
|
|
useEffect(() => {
|
|
if (!autoSubmit) requested.current = false;
|
|
if (autoSubmit && !disabled && !requested.current && form.current) {
|
|
requested.current = true;
|
|
form.current.requestSubmit();
|
|
}
|
|
}, [autoSubmit, disabled]);
|
|
return (
|
|
<form
|
|
ref={form}
|
|
method="post"
|
|
action={gitHubAppManifestAction(registration)}
|
|
onSubmit={onSubmit}
|
|
>
|
|
<input type="hidden" name="manifest" value={JSON.stringify(registration.manifest)} />
|
|
<SetupWizardFooter onSaveExit={onSaveExit} disabled={disabled}>
|
|
<Button type="submit" disabled={disabled}>Continue to GitHub</Button>
|
|
</SetupWizardFooter>
|
|
</form>
|
|
);
|
|
}
|
|
export function GitHubChatSetup() {
|
|
const [params, setParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const { selectedCompanyId } = useCompany();
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
|
const queryClient = useQueryClient();
|
|
const resume = params.get("resume");
|
|
const identityOnly = params.get("stage") === "identity";
|
|
const reconnect = params.get("reconnect") === "1";
|
|
const [agentId, setAgentId] = useState(params.get("agentId") ?? "");
|
|
const [ownerType, setOwnerType] = useState<"personal" | "organization">(
|
|
"personal",
|
|
);
|
|
const [ownerLogin, setOwnerLogin] = useState("");
|
|
const [customOrganization, setCustomOrganization] = useState(false);
|
|
const [name, setName] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
const [submitManifest, setSubmitManifest] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [existing, setExisting] = useState(params.get("reconnect") === "1");
|
|
const [appNotCreated, setAppNotCreated] = useState(false);
|
|
const [legacyAccount, setLegacyAccount] = useState("");
|
|
const [identityLinked, setIdentityLinked] = useState(false);
|
|
const [legacyIdentity, setLegacyIdentity] = useState<Awaited<
|
|
ReturnType<typeof githubChatApi.identity>
|
|
> | null>(null);
|
|
const agents = useQuery({
|
|
queryKey: ["github-setup-agents", selectedCompanyId],
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && !identityOnly,
|
|
});
|
|
const current = useQuery({
|
|
queryKey: ["github-setup", resume],
|
|
queryFn: () => chatEndpointsApi.get(resume!),
|
|
enabled: !!resume,
|
|
refetchInterval: 3000,
|
|
});
|
|
const bot = current.data;
|
|
const progress = useQuery({
|
|
queryKey: ["github-wizard", resume],
|
|
queryFn: () => githubChatApi.advance(resume!),
|
|
enabled: !!bot && !existing && !identityOnly,
|
|
refetchInterval: (query) =>
|
|
["connected", "create", "identity", "enrollment", "recovery"].includes(
|
|
query.state.data?.state ?? "",
|
|
)
|
|
? false
|
|
: 5000,
|
|
retry: false,
|
|
});
|
|
const accounts = useQuery({
|
|
queryKey: ["github-personal-connections", resume],
|
|
queryFn: () => githubChatApi.personalConnections(resume!),
|
|
enabled: !!bot && (identityOnly || progress.data?.state === "identity"),
|
|
});
|
|
const selectedAgent = agents.data?.find(
|
|
(agent) => agent.id === (bot?.assignedAgentId ?? agentId),
|
|
);
|
|
const state = identityOnly ? undefined : progress.data;
|
|
const choosingAccount = !!bot && !identityOnly && !existing && state?.state === "create";
|
|
const repositories = useQuery({
|
|
queryKey: ["project-repositories", selectedCompanyId],
|
|
queryFn: () => projectsApi.repositoryOptions(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && choosingAccount,
|
|
staleTime: 30_000,
|
|
});
|
|
const connectedBots = useQuery({
|
|
queryKey: queryKeys.chatEndpoints.list(selectedCompanyId!),
|
|
queryFn: () => chatEndpointsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && choosingAccount,
|
|
staleTime: 30_000,
|
|
});
|
|
const organizations = new Map<string, string>();
|
|
for (const repository of repositories.data?.repositories ?? []) {
|
|
if (repository.ownerType !== "organization") continue;
|
|
const login = repository.fullName.split("/")[0];
|
|
organizations.set(login.toLowerCase(), login);
|
|
}
|
|
for (const endpoint of connectedBots.data ?? []) {
|
|
const github = endpoint.setup?.github;
|
|
if (endpoint.provider === "github" && ["active", "paused"].includes(endpoint.status) &&
|
|
github?.ownerType === "organization" && github.ownerLogin) {
|
|
organizations.set(github.ownerLogin.toLowerCase(), github.ownerLogin);
|
|
}
|
|
}
|
|
const knownOrganizations = [...organizations.values()].sort((a, b) => a.localeCompare(b));
|
|
const knownOwner = organizations.get(ownerLogin.toLowerCase());
|
|
const accountChoice = ownerType === "personal" ? "personal"
|
|
: !customOrganization && knownOwner ? `organization:${knownOwner}` : "organization";
|
|
const deliveryVerifiedWhileChecking =
|
|
state?.state === "verify" &&
|
|
!!bot?.setup?.webhookVerifiedAt &&
|
|
state.verification?.checks.some(
|
|
(check) => !check.ok && ["webhook", "delivery"].includes(check.key),
|
|
);
|
|
const existingIdentityMethod =
|
|
identityOnly || state?.identityMethod === "existing_connection";
|
|
const connected = state?.state === "connected" && !existing;
|
|
useEffect(() => {
|
|
if (!connected || !bot) return;
|
|
queryClient.setQueryData(queryKeys.chatEndpoints.detail(bot.id), bot);
|
|
if (selectedAgent) queryClient.setQueryData(queryKeys.agents.detail(selectedAgent.id), selectedAgent);
|
|
navigate(`/apps/chat/${bot.id}/settings`, {
|
|
replace: true,
|
|
state: {
|
|
githubConnectionComplete: { endpointId: bot.id, runtimeChecks: state.runtimeChecks },
|
|
} satisfies GitHubConnectionCompleteLocationState,
|
|
});
|
|
}, [connected, bot, selectedAgent, state, queryClient, navigate]);
|
|
useEffect(() => {
|
|
setBreadcrumbs([
|
|
{ label: "Connectors", href: "/apps" },
|
|
{ label: "GitHub Code Review Bot" },
|
|
]);
|
|
return () => setBreadcrumbs([]);
|
|
}, [setBreadcrumbs]);
|
|
useEffect(() => {
|
|
if (bot) {
|
|
setAgentId(bot.assignedAgentId);
|
|
if (bot.setup?.github?.ownerType)
|
|
setOwnerType(bot.setup.github.ownerType);
|
|
setOwnerLogin(bot.setup?.github?.ownerLogin ?? "");
|
|
setCustomOrganization(false);
|
|
if (bot.setup?.github?.appName) setName(bot.setup.github.appName);
|
|
}
|
|
}, [
|
|
bot?.id,
|
|
bot?.setup?.github?.ownerType,
|
|
bot?.setup?.github?.ownerLogin,
|
|
bot?.setup?.github?.appName,
|
|
]);
|
|
async function run(action: () => Promise<void>) {
|
|
setBusy(true);
|
|
setError("");
|
|
try {
|
|
await action();
|
|
} catch (error) {
|
|
setError(
|
|
error instanceof Error
|
|
? error.message
|
|
: "Could not connect GitHub. Try again.",
|
|
);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
async function refresh() {
|
|
const verification = !identityOnly ? await progress.refetch() : undefined;
|
|
const updated = await current.refetch();
|
|
if (verification?.error) throw verification.error;
|
|
if (updated.error) throw updated.error;
|
|
}
|
|
const appInput = () => ({
|
|
name: (
|
|
name ||
|
|
selectedAgent?.name ||
|
|
bot?.assignedAgentName ||
|
|
"GitHub Reviewer"
|
|
).slice(0, 34),
|
|
ownerType,
|
|
...(ownerType === "organization" ? { ownerLogin: ownerLogin.trim() } : {}),
|
|
});
|
|
const exit = () =>
|
|
void run(async () => {
|
|
if (
|
|
!identityOnly &&
|
|
!reconnect &&
|
|
bot &&
|
|
!bot.botExternalId &&
|
|
!state?.registration &&
|
|
state?.state !== "recovery"
|
|
) {
|
|
await githubChatApi.saveDraft(bot.id, appInput());
|
|
await current.refetch();
|
|
}
|
|
navigate("/apps");
|
|
});
|
|
const footer = (
|
|
label?: string,
|
|
action?: () => Promise<void>,
|
|
disabled = false,
|
|
) => (
|
|
<SetupWizardFooter onSaveExit={exit} disabled={busy}>
|
|
{label && action && (
|
|
<Button disabled={busy || disabled} onClick={() => void run(action)}>
|
|
{busy && <Loader2 className="mr-2 size-4 animate-spin" />}
|
|
{label}
|
|
</Button>
|
|
)}
|
|
</SetupWizardFooter>
|
|
);
|
|
if (resume && !bot)
|
|
return (
|
|
<div className="mx-auto w-full max-w-3xl space-y-6 px-4 py-6 sm:px-6">
|
|
<h1 className="text-2xl font-semibold">Resume GitHub setup</h1>
|
|
{current.error ? (
|
|
<p role="alert" className="text-sm text-destructive">
|
|
Could not load this connection. Try again to resume your draft.
|
|
</p>
|
|
) : (
|
|
<p role="status" className="text-sm text-muted-foreground">
|
|
Loading your draft…
|
|
</p>
|
|
)}
|
|
{footer(current.error ? "Try again" : undefined, async () => {
|
|
await current.refetch();
|
|
})}
|
|
</div>
|
|
);
|
|
if (connected) return null;
|
|
return (
|
|
<div className="mx-auto w-full max-w-3xl space-y-6 px-4 py-6 sm:px-6">
|
|
{!identityOnly && (
|
|
<SetupWizardNavigation
|
|
labels={reconnect ? ["Reconnect GitHub"] : ["Choose agent", "Connect GitHub"]}
|
|
step={reconnect ? 0 : bot ? 1 : 0}
|
|
availableStep={reconnect ? 0 : bot ? 1 : 0}
|
|
onSelect={() => {}}
|
|
disabled
|
|
takeover
|
|
/>
|
|
)}
|
|
<h1 className="text-2xl font-semibold">
|
|
{!bot
|
|
? "Choose agent"
|
|
: identityOnly || state?.state === "identity"
|
|
? "Connect your account"
|
|
: reconnect && existing ? "Reconnect GitHub" : "Connect GitHub"}
|
|
</h1>
|
|
{(error || (!identityOnly && !existing && progress.error) || accounts.error) && (
|
|
<p role="alert" className="text-sm text-destructive">
|
|
{error ||
|
|
(accounts.error instanceof Error
|
|
? accounts.error.message
|
|
: undefined) ||
|
|
(progress.error instanceof Error
|
|
? progress.error.message
|
|
: "Could not check GitHub setup. Try again.")}
|
|
</p>
|
|
)}
|
|
{!bot ? (
|
|
<>
|
|
<AgentSelect
|
|
agents={agents.data ?? []}
|
|
value={agentId}
|
|
onChange={setAgentId}
|
|
/>
|
|
{selectedAgent && (
|
|
<GitHubAgentTrustWarning
|
|
agent={selectedAgent}
|
|
disabled={busy}
|
|
onChangeToLowTrust={() =>
|
|
void run(async () => {
|
|
const permissions = buildPermissionsForTrustPreset(
|
|
selectedAgent.permissions,
|
|
"low_trust_review",
|
|
);
|
|
await agentsApi.updatePermissions(
|
|
selectedAgent.id,
|
|
{
|
|
...permissions,
|
|
canCreateAgents: false,
|
|
canCreateSkills: false,
|
|
canAssignTasks:
|
|
typeof selectedAgent.permissions.canAssignTasks ===
|
|
"boolean"
|
|
? selectedAgent.permissions.canAssignTasks
|
|
: true,
|
|
},
|
|
selectedCompanyId!,
|
|
);
|
|
await queryClient.invalidateQueries({
|
|
queryKey: ["github-setup-agents", selectedCompanyId],
|
|
});
|
|
})
|
|
}
|
|
/>
|
|
)}
|
|
{footer(
|
|
"Continue",
|
|
async () => {
|
|
const created = await chatEndpointsApi.create(
|
|
selectedCompanyId!,
|
|
{ provider: "github", assignedAgentId: agentId },
|
|
);
|
|
setName((selectedAgent?.name ?? "GitHub Reviewer").slice(0, 34));
|
|
const next = new URLSearchParams(params);
|
|
next.set("resume", created.id);
|
|
setParams(next, { replace: true });
|
|
queryClient.setQueryData(["github-setup", created.id], created);
|
|
},
|
|
!agentId || !selectedCompanyId,
|
|
)}
|
|
</>
|
|
) : !identityOnly && !existing && progress.isPending ? (
|
|
<>
|
|
<p role="status" className="text-sm text-muted-foreground">
|
|
Checking your GitHub connection…
|
|
</p>
|
|
{footer()}
|
|
</>
|
|
) : existing ? (
|
|
<GitHubAppRecovery
|
|
key={bot.id}
|
|
endpoint={bot}
|
|
reconnect={reconnect}
|
|
onExit={exit}
|
|
onConnect={async (credentials) => {
|
|
if (reconnect || ["active", "paused", "revoked"].includes(bot.status)) {
|
|
await chatEndpointsApi.setup(bot.id, {
|
|
action: "reconnect",
|
|
...(credentials ? { credentials: { appId: credentials.appId.trim(), privateKey: credentials.privateKey } } : {}),
|
|
});
|
|
} else if (credentials) {
|
|
await githubChatApi.connectApp(bot.id, credentials);
|
|
}
|
|
queryClient.removeQueries({ queryKey: ["github-wizard", bot.id] });
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.chatEndpoints.detail(bot.id) });
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.chatEndpoints.list(bot.companyId) });
|
|
await refresh();
|
|
setExisting(false);
|
|
}}
|
|
/>
|
|
) : state?.state === "identity" || identityOnly ? (
|
|
<>
|
|
{identityOnly && identityLinked ? (
|
|
<>
|
|
<p role="status" className="text-sm">
|
|
GitHub account <strong>{legacyIdentity?.login}</strong>{" "}
|
|
connected. You can now mention this bot on GitHub.
|
|
</p>
|
|
{footer()}
|
|
</>
|
|
) : state?.identity ? (
|
|
<>
|
|
<p className="text-sm">
|
|
Connect GitHub account <strong>{state.identity.login}</strong>{" "}
|
|
to your Paperclip account.
|
|
</p>
|
|
{footer("Confirm my account", async () => {
|
|
await githubChatApi.confirmIdentity(
|
|
bot.id,
|
|
state.identity!.githubUserId,
|
|
);
|
|
await refresh();
|
|
})}
|
|
</>
|
|
) : (
|
|
<>
|
|
<p className="text-sm text-muted-foreground">
|
|
Link your GitHub account so your mentions use your Paperclip
|
|
permissions.
|
|
</p>
|
|
{(accounts.data?.length ?? 0) > 0 && (
|
|
<>
|
|
<Label htmlFor="github-personal-account">
|
|
Existing GitHub connection
|
|
</Label>
|
|
<select
|
|
id="github-personal-account"
|
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
value={legacyAccount}
|
|
onChange={(event) => {
|
|
setLegacyAccount(event.target.value);
|
|
setLegacyIdentity(null);
|
|
setIdentityLinked(false);
|
|
}}
|
|
>
|
|
<option value="">Choose an account</option>
|
|
{accounts.data
|
|
?.filter(
|
|
(account) =>
|
|
account.status === "active" && account.enabled,
|
|
)
|
|
.map((account) => (
|
|
<option
|
|
key={account.connectionId}
|
|
value={account.connectionId}
|
|
>
|
|
{account.login ?? account.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{legacyIdentity && (
|
|
<p className="text-sm">
|
|
GitHub account: <strong>{legacyIdentity.login}</strong>
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
{existingIdentityMethod && !legacyAccount && (
|
|
<Link
|
|
className="text-sm underline"
|
|
to="/apps/connect?source=github"
|
|
>
|
|
Add a personal GitHub connection, then return to this draft
|
|
</Link>
|
|
)}
|
|
{footer(
|
|
legacyAccount
|
|
? legacyIdentity
|
|
? "Confirm my account"
|
|
: "Check my account"
|
|
: "Connect my GitHub account",
|
|
async () => {
|
|
if (legacyAccount) {
|
|
const observed = await githubChatApi.identity(
|
|
bot.id,
|
|
legacyAccount,
|
|
legacyIdentity?.githubUserId,
|
|
);
|
|
setLegacyIdentity(observed);
|
|
if (legacyIdentity) {
|
|
setIdentityLinked(true);
|
|
await refresh();
|
|
}
|
|
} else {
|
|
const result = await githubChatApi.startIdentity(bot.id);
|
|
window.location.assign(result.authorizationUrl);
|
|
}
|
|
},
|
|
existingIdentityMethod && !legacyAccount,
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
) : state?.state === "install" ? (
|
|
<>
|
|
<p role="status" className="text-sm text-muted-foreground">
|
|
Waiting for GitHub installation or administrator approval…
|
|
</p>
|
|
{state.installationUrl && (
|
|
<a
|
|
className="inline-flex items-center gap-1 text-sm underline"
|
|
href={state.installationUrl}
|
|
>
|
|
Continue installation on GitHub
|
|
<ExternalLink className="size-3" />
|
|
</a>
|
|
)}
|
|
{footer()}
|
|
</>
|
|
) : state?.state === "enrollment" ? (
|
|
<>
|
|
<p className="text-sm">
|
|
Paperclip Cloud receives GitHub events and securely delivers them to
|
|
this instance.
|
|
</p>
|
|
{footer("Connect Paperclip Cloud", async () => {
|
|
const result = await toolsApi.startCloudConnectorEnrollment(
|
|
selectedCompanyId!,
|
|
undefined,
|
|
`/apps/chat/connect${window.location.search}`,
|
|
);
|
|
if (!result.verificationUrl)
|
|
throw new Error("Cloud enrollment could not be started");
|
|
window.location.assign(result.verificationUrl);
|
|
})}
|
|
</>
|
|
) : state?.state === "recovery" ? (
|
|
<>
|
|
<p role="status" className="text-sm">
|
|
{state.message}
|
|
</p>
|
|
<Button variant="ghost" onClick={() => setExisting(true)}>
|
|
Use existing App credentials
|
|
</Button>
|
|
{state.restartableRegistrationId && (
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox id="github-app-not-created" checked={appNotCreated} disabled={busy}
|
|
onCheckedChange={(checked) => setAppNotCreated(checked === true)} />
|
|
<Label htmlFor="github-app-not-created">I haven't created this App on GitHub.</Label>
|
|
</div>
|
|
)}
|
|
{state.restartableRegistrationId
|
|
? footer("Continue to GitHub", async () => {
|
|
const result = await githubChatApi.restartRegistration(bot.id, state.restartableRegistrationId!);
|
|
setAppNotCreated(false);
|
|
queryClient.setQueryData(["github-wizard", bot.id], result);
|
|
if (result.registration) setSubmitManifest(true);
|
|
}, !appNotCreated)
|
|
: footer("Try again", refresh)}
|
|
</>
|
|
) : state?.state === "verify" ? (
|
|
<>
|
|
<p role="status" className="text-sm text-muted-foreground">
|
|
{deliveryVerifiedWhileChecking
|
|
? "GitHub delivery verified. Checking App and repository access…"
|
|
: state.message ?? "Checking GitHub access…"}
|
|
</p>
|
|
{state.verification?.checks
|
|
.filter(
|
|
(check) =>
|
|
!check.ok &&
|
|
!(deliveryVerifiedWhileChecking && ["webhook", "delivery"].includes(check.key)),
|
|
)
|
|
.map((check) => (
|
|
<p key={check.key} className="text-sm">
|
|
{check.detail}
|
|
</p>
|
|
))}
|
|
<Link
|
|
className="text-sm underline"
|
|
to={`/apps/chat/${bot.id}/settings`}
|
|
>
|
|
Connection settings
|
|
</Link>
|
|
{footer("Try again", refresh)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="github-owner-type">GitHub account</Label>
|
|
<select
|
|
id="github-owner-type"
|
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
value={accountChoice}
|
|
disabled={busy || !!state?.registration}
|
|
onChange={(event) => {
|
|
const choice = event.target.value;
|
|
setOwnerType(choice === "personal" ? "personal" : "organization");
|
|
setCustomOrganization(choice === "organization");
|
|
if (choice.startsWith("organization:")) setOwnerLogin(choice.slice("organization:".length));
|
|
else if (choice === "organization") setOwnerLogin("");
|
|
}}
|
|
>
|
|
<option value="personal">My account</option>
|
|
{knownOrganizations.map((login) => (
|
|
<option key={login.toLowerCase()} value={`organization:${login}`}>{login}</option>
|
|
))}
|
|
<option value="organization">Another organization</option>
|
|
</select>
|
|
{(repositories.isError || connectedBots.isError) && (
|
|
<p role="alert" className="text-sm text-destructive">
|
|
Could not load some connected GitHub accounts. Choose Another organization to enter a name.
|
|
</p>
|
|
)}
|
|
</div>
|
|
{accountChoice === "organization" && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="github-owner-login">Organization</Label>
|
|
<Input
|
|
id="github-owner-login"
|
|
disabled={busy || !!state?.registration}
|
|
value={ownerLogin}
|
|
onChange={(event) => setOwnerLogin(event.target.value)}
|
|
placeholder="GitHub organization name"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="github-app-name">App name</Label>
|
|
<Input
|
|
id="github-app-name"
|
|
disabled={busy || !!state?.registration}
|
|
maxLength={34}
|
|
value={
|
|
name ||
|
|
selectedAgent?.name?.slice(0, 34) ||
|
|
bot.assignedAgentName?.slice(0, 34) ||
|
|
"GitHub Reviewer"
|
|
}
|
|
onChange={(event) => setName(event.target.value)}
|
|
/>
|
|
</div>
|
|
<button
|
|
className="text-sm text-muted-foreground underline"
|
|
onClick={() => setExisting(true)}
|
|
>
|
|
I already have an App
|
|
</button>
|
|
{state?.registration ? (
|
|
<GitHubAppManifestForm registration={state.registration} onSaveExit={exit}
|
|
disabled={busy} autoSubmit={submitManifest} onSubmit={() => setSubmitManifest(false)} />
|
|
) : footer(
|
|
"Continue to GitHub",
|
|
async () => {
|
|
const result = await githubChatApi.registration(bot.id, appInput());
|
|
queryClient.setQueryData(["github-wizard", bot.id], result);
|
|
if (result.registration) setSubmitManifest(true);
|
|
},
|
|
progress.isPending || (ownerType === "organization" && !ownerLogin.trim()),
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|