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