Canvas: Github repository picker
Change-Id: Icb8f2ffbef2894b2fdea4e4c13c74c0f4970506b
diff --git a/apps/canvas/front/src/components/canvas.tsx b/apps/canvas/front/src/components/canvas.tsx
index 2f261f0..8898b58 100644
--- a/apps/canvas/front/src/components/canvas.tsx
+++ b/apps/canvas/front/src/components/canvas.tsx
@@ -47,9 +47,9 @@
return c.targetHandle === "repository";
}
if (sn.type === "app") {
- if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
+ if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
return false;
- }
+ }
}
if (tn.type === "gateway-https") {
if (c.targetHandle === "https" && tn.data.https !== undefined) {
@@ -81,7 +81,7 @@
x: 0,
y: 0,
},
- isConnectable: true,
+ isConnectable: true,
data: {
domain: n.domain,
label: n.domain,
@@ -109,7 +109,7 @@
isValidConnection={isValidConnection}
fitView
proOptions={{ hideAttribution: true }}
- >
+ >
<Controls />
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
<Panel position="bottom-right">
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index dcccd82..5b4b3fa 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -394,8 +394,8 @@
</SelectTrigger>
</FormControl>
<SelectContent>
- {nodes.filter((n) => n.type === "github" && n.data.address).map((n) => (
- <SelectItem key={n.id} value={n.id}>{`${n.data.address!}`}</SelectItem>
+ {nodes.filter((n) => n.type === "github" && n.data.repository?.id !== undefined).map((n) => (
+ <SelectItem key={n.id} value={n.id}>{`${n.data.repository?.sshURL}`}</SelectItem>
))}
</SelectContent>
</Select>
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index aa48cd1..3ae779e 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -1,12 +1,16 @@
import { NodeRect } from './node-rect';
-import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore } from '@/lib/state';
-import { useEffect, useMemo } from 'react';
+import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore, useGithubService } from '@/lib/state';
+import { useEffect, useMemo, useState } from 'react';
import { z } from "zod";
import { DeepPartial, EventType, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
-import { Input } from './ui/input';
import { Handle, Position } from "@xyflow/react";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
+import { GitHubRepository } from '../lib/github';
+import { useProjectId } from '@/lib/state';
+import { Alert, AlertDescription } from './ui/alert';
+import { AlertCircle } from 'lucide-react';
export function NodeGithub(node: GithubNode) {
const { id, selected } = node;
@@ -22,54 +26,136 @@
isConnectableStart={isConnectable}
isConnectableEnd={isConnectable}
isConnectable={isConnectable}
- />
+ />
</div>
</NodeRect>
);
}
const schema = z.object({
- address: z.string().min(1),
+ repositoryId: z.number().optional(),
});
export function NodeGithubDetails(node: GithubNode) {
const { id, data } = node;
const store = useStateStore();
+ const projectId = useProjectId();
+ const [repos, setRepos] = useState<GitHubRepository[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+ const githubService = useGithubService();
+
+ useEffect(() => {
+ if (data.repository) {
+ const { id, sshURL } = data.repository;
+ setRepos(prevRepos => {
+ if (!prevRepos.some(r => r.id === id)) {
+ return [...prevRepos, {
+ id,
+ name: sshURL.split('/').pop() || '',
+ full_name: sshURL.split('/').slice(-2).join('/'),
+ html_url: '',
+ ssh_url: sshURL,
+ description: null,
+ private: false,
+ default_branch: 'main'
+ }];
+ }
+ return prevRepos;
+ });
+ }
+ }, [data.repository]);
+
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
mode: "onChange",
defaultValues: {
- address: data.address,
+ repositoryId: data.repository?.id,
}
});
+
useEffect(() => {
const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name, type }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
if (type !== "change") {
return;
}
switch (name) {
- case "address":
- store.updateNodeData<"github">(id, {
- address: value.address,
- });
+ case "repositoryId":
+ if (value.repositoryId) {
+ const repo = repos.find(r => r.id === value.repositoryId);
+ if (repo) {
+ store.updateNodeData<"github">(id, {
+ repository: {
+ id: repo.id,
+ sshURL: repo.ssh_url,
+ },
+ });
+ }
+ }
break;
}
});
return () => sub.unsubscribe();
- }, [form, store]);
+ }, [form, store, id, repos]);
+
+ useEffect(() => {
+ const fetchRepositories = async () => {
+ if (!!!githubService) return;
+
+ setLoading(true);
+ setError(null);
+ try {
+ const repositories = await githubService.getRepositories();
+ setRepos(repositories);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to fetch repositories");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchRepositories();
+ }, [githubService]);
+
return (
<>
<Form {...form}>
<form className="space-y-2">
- <FormField
+ <FormField
control={form.control}
- name="address"
+ name="repositoryId"
render={({ field }) => (
<FormItem>
- <FormControl>
- <Input placeholder="address" className="border border-black" {...field} />
- </FormControl>
+ <Select
+ onValueChange={(value) => field.onChange(Number(value))}
+ value={field.value?.toString()}
+ disabled={loading || !projectId || !!!githubService}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder={!!githubService ? "Select a repository" : "GitHub not configured"} />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {repos.map((repo) => (
+ <SelectItem key={repo.id} value={repo.id.toString()}>
+ {repo.full_name}
+ {repo.description && ` - ${repo.description}`}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
<FormMessage />
+ {error && <p className="text-sm text-red-500">{error}</p>}
+ {loading && <p className="text-sm text-gray-500">Loading repositories...</p>}
+ {!!!githubService && (
+ <Alert variant="destructive" className="mt-2">
+ <AlertCircle className="h-4 w-4" />
+ <AlertDescription>
+ GitHub access token is not configured. Please configure it in the Integrations tab.
+ </AlertDescription>
+ </Alert>
+ )}
</FormItem>
)}
/>
diff --git a/apps/canvas/front/src/components/ui/alert.tsx b/apps/canvas/front/src/components/ui/alert.tsx
new file mode 100644
index 0000000..350d1cd
--- /dev/null
+++ b/apps/canvas/front/src/components/ui/alert.tsx
@@ -0,0 +1,58 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
+>(({ className, variant, ...props }, ref) => (
+ <div
+ ref={ref}
+ role="alert"
+ className={cn(alertVariants({ variant }), className)}
+ {...props}
+ />
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+ <h5
+ ref={ref}
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
+ {...props}
+ />
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+ <div
+ ref={ref}
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
+ {...props}
+ />
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
\ No newline at end of file