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
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import type React from "react";
import { useState } from "react";
import Link from "next/link";
import {
IconBrandGithub,
IconLoader2,
IconSelector,
} from "@tabler/icons-react";

import { Avatar, AvatarFallback, AvatarImage } from "@ctrlplane/ui/avatar";
import { Button } from "@ctrlplane/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ctrlplane/ui/command";
import { Input } from "@ctrlplane/ui/input";
import { Label } from "@ctrlplane/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover";

import { api } from "~/trpc/react";

export const DeploymentJobAgentGithubConfig: React.FC<{
jobAgentId: string;
value: Record<string, any>;
onChange: (v: Record<string, any>) => void;
}> = ({ jobAgentId, value, onChange }) => {
const [repoOpen, setRepoOpen] = useState(false);
const [workflowOpen, setWorkflowOpen] = useState(false);

const { data: githubAgent, isLoading: isGithubAgentLoading } =
api.job.agent.github.byId.useQuery(jobAgentId);
const { data: repos, isLoading: isReposLoading } =
api.github.entities.repos.list.useQuery(
{
installationId: githubAgent?.ghEntity.installationId ?? 0,
owner: githubAgent?.ghEntity.slug ?? "",
workspaceId: githubAgent?.workspaceId ?? "",
},
{ enabled: githubAgent != null },
);

const selectedRepo = repos?.find((r) => r.name === value.repo);
const workflows = selectedRepo?.workflows ?? [];
const selectedWorkflow = workflows.find((w) => w.id === value.workflowId);

if (isGithubAgentLoading)
return (
<div className="flex w-96 items-center justify-center gap-2">
<IconLoader2 className="animate-spin" />
</div>
);

return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Avatar className="size-14">
<AvatarImage src={githubAgent?.ghEntity.avatarUrl ?? ""} />
<AvatarFallback>
<IconBrandGithub />
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-2xl font-semibold">
{githubAgent?.ghEntity.slug}
</span>
<Link
href={`https://github.com/${githubAgent?.ghEntity.slug ?? ""}`}
className="text-sm hover:text-primary"
target="_blank"
rel="noopener noreferrer"
>
View on GitHub
</Link>
</div>
</div>

<div className="flex w-96 flex-col gap-6">
<div className="flex flex-col gap-2">
<Label className="font-medium">Repository</Label>
<Popover open={repoOpen} onOpenChange={setRepoOpen} modal>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={repoOpen}
className="items-center justify-start gap-2 px-2"
>
<IconSelector className="h-4 w-4" />
<span className="overflow-hidden text-ellipsis">
{selectedRepo?.name ?? "Select repo..."}
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder="Search repo..." />
<CommandGroup>
<CommandList className="scrollbar-thin scrollbar-track-neutral-800 scrollbar-thumb-neutral-700">
{!isReposLoading &&
repos != null &&
repos.length > 0 &&
repos.map((repo) => (
<CommandItem
key={repo.id}
value={repo.name}
onSelect={(currentValue) => {
onChange({ ...value, repo: currentValue });
setRepoOpen(false);
}}
>
{repo.name}
</CommandItem>
))}

{!isReposLoading &&
(repos == null || repos.length === 0) && (
<CommandEmpty className="flex justify-center py-2 text-sm text-muted-foreground">
No repos found
</CommandEmpty>
)}

{isReposLoading && (
<CommandItem>
<IconLoader2 className="h-4 w-4 animate-spin" /> Loading
repos...
</CommandItem>
)}
</CommandList>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>

<div className="flex flex-col gap-2">
<Label className="font-medium">Workflow</Label>
<Popover open={workflowOpen} onOpenChange={setWorkflowOpen} modal>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={workflowOpen}
className="items-center justify-start gap-2 px-2"
>
<IconSelector className="h-4 w-4" />
<span className="overflow-hidden text-ellipsis">
{selectedWorkflow?.name ?? "Select workflow..."}
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder="Search workflow..." />
<CommandGroup>
<CommandList>
{workflows.length > 0 &&
workflows.map((wf) => (
<CommandItem
key={wf.id}
value={wf.id.toString()}
onSelect={(currentValue) => {
onChange({
...value,
workflowId: Number.parseInt(currentValue),
});
setWorkflowOpen(false);
}}
>
{wf.name}
</CommandItem>
))}

{workflows.length === 0 && (
<CommandEmpty className="flex justify-center py-2 text-sm text-muted-foreground">
No workflows found
</CommandEmpty>
)}
</CommandList>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>

<div className="flex flex-col gap-2">
<Label className="font-medium">Git reference</Label>
<Input
placeholder="(uses repositories default if not set)"
value={value.ref ?? ""}
onChange={(e) => {
const ref = e.target.value === "" ? null : e.target.value;
onChange({ ...value, ref });
}}
/>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { z } from "zod";

import { Alert, AlertDescription, AlertTitle } from "@ctrlplane/ui/alert";
import { Button } from "@ctrlplane/ui/button";
import { Card } from "@ctrlplane/ui/card";
import { Form, FormField, useForm } from "@ctrlplane/ui/form";
import { JobAgentType } from "@ctrlplane/validators/jobs";

import { JobAgentConfig } from "~/components/form/job-agent/JobAgentConfig";
import { JobAgentKubernetesConfig } from "~/components/form/job-agent/JobAgentKubernetesConfig";
import { JobAgentScriptConfig } from "~/components/form/job-agent/JobAgentScriptConfig";
import { JobAgentSelector } from "~/components/form/job-agent/JobAgentSelector";
import { api } from "~/trpc/react";
import { DeploymentJobAgentGithubConfig } from "./DeploymentJobAgentGithubConfig";

const JobAgentForm: React.FC<{
jobAgent?: schema.JobAgent;
Expand Down Expand Up @@ -41,7 +43,7 @@ const JobAgentForm: React.FC<{

return (
<Form {...form}>
<form onSubmit={onFormSubmit} className="space-y-3">
<form onSubmit={onFormSubmit} className="space-y-6">
<FormField
control={form.control}
name="jobAgentId"
Expand All @@ -55,26 +57,39 @@ const JobAgentForm: React.FC<{
/>
)}
/>
<Card className="rounded-md border-neutral-900 p-4">
<FormField
control={form.control}
name="jobAgentConfig"
render={({ field: { value, onChange } }) =>
selectedJobAgent == null ? (

<FormField
control={form.control}
name="jobAgentConfig"
render={({ field }) => (
<>
{selectedJobAgent == null && (
<span className="px-2 text-sm text-muted-foreground">
Select a job agent
</span>
) : (
<JobAgentConfig
jobAgent={selectedJobAgent}
workspace={workspace}
value={value}
onChange={onChange}
)}
{selectedJobAgent?.type === JobAgentType.KubernetesJob && (
<JobAgentKubernetesConfig {...field} />
)}
{selectedJobAgent?.type === JobAgentType.GithubApp && (
<DeploymentJobAgentGithubConfig
jobAgentId={jobAgentId}
{...field}
/>
)
}
/>
</Card>
)}
{selectedJobAgent?.type.startsWith("exec-") && (
<JobAgentScriptConfig
type={
selectedJobAgent.type.startsWith(JobAgentType.ExecWindows)
? "powershell"
: "shell"
}
{...field}
/>
)}
</>
)}
/>

<Button
type="submit"
Expand Down
51 changes: 41 additions & 10 deletions packages/api/src/router/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,28 @@ const getRepos = async (
installationOctokit: InstallationOctokitClient,
installationToken: { token: string },
owner: string,
type: "organization" | "user",
) => {
const { data } = await installationOctokit.repos.listForOrg({
org: owner,
per_page: 100,
page,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
authorization: `Bearer ${installationToken.token}`,
},
});
const { data } =
type === "organization"
? await installationOctokit.repos.listForOrg({
org: owner,
per_page: 100,
page,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
authorization: `Bearer ${installationToken.token}`,
},
})
: await installationOctokit.repos.listForUser({
username: owner,
per_page: 100,
page,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
authorization: `Bearer ${installationToken.token}`,
},
});

const reposWithWorkflows = await Promise.all(
data.map(async (repo) => {
Expand All @@ -157,6 +169,7 @@ const getRepos = async (
installationOctokit,
installationToken,
owner,
type,
);
};

Expand All @@ -176,7 +189,24 @@ const reposRouter = createTRPCRouter({
workspaceId: z.string().uuid(),
}),
)
.query(async ({ input }) => {
.query(async ({ ctx, input }) => {
const entity = await ctx.db
.select()
.from(githubEntity)
.where(
and(
eq(githubEntity.installationId, input.installationId),
eq(githubEntity.workspaceId, input.workspaceId),
),
)
.then(takeFirstOrNull);

if (entity == null)
throw new TRPCError({
code: "NOT_FOUND",
message: "Entity not found",
});

const { data: installation } = await getOctokit().apps.getInstallation({
installation_id: input.installationId,
});
Expand All @@ -193,6 +223,7 @@ const reposRouter = createTRPCRouter({
installationOctokit,
installationToken,
input.owner,
entity.type,
);
}),
});
Expand Down
Loading
Loading