Canvas: Github repository picker

Change-Id: Icb8f2ffbef2894b2fdea4e4c13c74c0f4970506b
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 02fd983..a3cd755 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,17 +1,18 @@
 import { v4 as uuidv4 } from "uuid";
 import { create } from 'zustand';
 import { addEdge, applyNodeChanges, applyEdgeChanges, Connection, EdgeChange, useNodes } from '@xyflow/react';
-import {
-  type Edge,
-  type Node,
-  type OnNodesChange,
-  type OnEdgesChange,
-  type OnConnect,
+import type {
+  Edge,
+  Node,
+  OnNodesChange,
+  OnEdgesChange,
+  OnConnect,
 } from '@xyflow/react';
-import { DeepPartial } from "react-hook-form";
+import type { DeepPartial } from "react-hook-form";
 import { Category, defaultCategories } from "./categories";
 import { CreateValidators, Validator } from "./config";
 import { z } from "zod";
+import { GitHubService, GitHubServiceImpl } from './github';
 
 export type InitData = {
   label: string;
@@ -135,7 +136,10 @@
 };
 
 export type GithubData = NodeData & {
-  address: string;
+  repository?: {
+    id: number;
+    sshURL: string;
+  };
 };
 
 export type GithubNode = Node<GithubData> & {
@@ -152,7 +156,7 @@
   switch (n.type) {
     case "network": return n.data.domain;
     case "app": return n.data.label || "Service";
-    case "github": return n.data.address || "Github";
+    case "github": return n.data.repository?.sshURL || "Github";
     case "gateway-https": {
       if (n.data && n.data.network && n.data.subdomain) {
         return `https://${n.data.subdomain}.${n.data.network}`;
@@ -189,7 +193,7 @@
       }
       return false;
     case "github":
-      if (n.data !== undefined && n.data.address) {
+      if (n.data.repository?.id !== undefined) {
         return true;
       }
       return false;
@@ -264,6 +268,7 @@
     case "postgresql": return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
     case "volume": return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
     case undefined: throw new Error("MUST NOT REACH");
+    default: throw new Error("MUST NOT REACH");
   }
 }
 
@@ -282,20 +287,38 @@
 };
 
 export const envSchema = z.object({
-  deployKey: z.string(),
+  deployKey: z.optional(z.string().min(1)),
   networks: z.array(z.object({
-    name: z.string(),
-    domain: z.string(),
-  })),
+    name: z.string().min(1),
+    domain: z.string().min(1),
+  })).default([]),
+  integrations: z.object({
+    github: z.boolean(),
+  }),
 });
 
 export type Env = z.infer<typeof envSchema>;
 
+const defaultEnv: Env = {
+  deployKey: undefined,
+  networks: [],
+  integrations: {
+    github: false,
+  }
+};
+
 export type Project = {
   id: string;
   name: string;
 }
 
+export type IntegrationsConfig = {
+  github: boolean;
+};
+
+type NodeUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>>;
+type NodeDataUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>["data"]>;
+
 export type AppState = {
   projectId: string | undefined;
   projects: Project[];
@@ -303,7 +326,8 @@
   edges: Edge[];
   categories: Category[];
   messages: Message[];
-  env?: Env;
+  env: Env;
+  githubService: GitHubService | null;
   setHighlightCategory: (name: string, active: boolean) => void;
   onNodesChange: OnNodesChange<AppNode>;
   onEdgesChange: OnEdgesChange;
@@ -312,15 +336,16 @@
   setEdges: (edges: Edge[]) => void;
   setProject: (projectId: string | undefined) => void;
   setProjects: (projects: Project[]) => void;
-  updateNode: <T extends NodeType>(id: string, data: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))>) => void;
-  updateNodeData: <T extends NodeType>(id: string, data: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))["data"]>) => void;
+  updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
+  updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
   replaceEdge: (c: Connection, id?: string) => void;
-  refreshEnv: () => Promise<Env | undefined>;
+  refreshEnv: () => Promise<void>;
 };
 
 const projectIdSelector = (state: AppState) => state.projectId;
 const categoriesSelector = (state: AppState) => state.categories;
 const messagesSelector = (state: AppState) => state.messages;
+const githubServiceSelector = (state: AppState) => state.githubService;
 const envSelector = (state: AppState) => state.env;
 
 export function useProjectId(): string | undefined {
@@ -347,88 +372,55 @@
   return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
 }
 
-let envRefresh: Promise<Env | undefined> | null = null;
-
-const fixedEnv: Env = {
-  "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
-  "networks": [{
-    "name": "Public",
-    "domain": "v1.dodo.cloud",
-  }, {
-    "name": "Private",
-    "domain": "p.v1.dodo.cloud",
-  }],
-};
-
 export function useEnv(): Env {
-  return fixedEnv;
-  const store = useStateStore();
-  const env = envSelector(store);
-  console.log(env);
-  if (env != null) {
-    return env;
-  }
-  if (envRefresh == null) {
-    envRefresh = store.refreshEnv();
-    envRefresh.finally(() => envRefresh = null);
-  }
-  return {
-    deployKey: "",
-    networks: [],
-  };
+  return useStateStore(envSelector);
+}
+
+export function useGithubService(): GitHubService | null {
+  return useStateStore(githubServiceSelector);
 }
 
 const v: Validator = CreateValidators();
 
 export const useStateStore = create<AppState>((set, get): AppState => {
-  set({
-    env: {
-      "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
-      "networks": [{
-        "name": "Public",
-        "domain": "v1.dodo.cloud",
-      }, {
-        "name": "Private",
-        "domain": "p.v1.dodo.cloud",
-      }],
-    }
-  });
-  console.log(get().env);
   const setN = (nodes: AppNode[]) => {
-    set({
-      nodes: nodes,
-      messages: v(nodes),
-    })
+    set((state) => ({
+      ...state,
+      nodes,
+    }));
   };
-  function updateNodeData<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))["data"]>): void {
-    setN(get().nodes.map((n) => {
-      if (n.id !== id) {
-        return n;
-      }
-      const nd = {
-        ...n,
-        data: {
-          ...n.data,
-          ...d,
-        },
-      };
-      return nd;
-    })
-    );
-  };
-  function updateNode<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))>): void {
+
+  function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
     setN(
       get().nodes.map((n) => {
-        if (n.id !== id) {
-          return n;
+        if (n.id === id) {
+          return {
+            ...n,
+            data: {
+              ...n.data,
+              ...data,
+            },
+          } as Extract<AppNode, { type: T }>;
         }
-        return {
-          ...n,
-          ...d,
-        };
+        return n;
       })
     );
-  };
+  }
+
+  function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
+    setN(
+      get().nodes.map((n) => {
+        if (n.id === id) {
+          return {
+            ...n,
+            ...node,
+          } as Extract<AppNode, { type: T }>;
+        }
+        return n;
+      })
+    );
+  }
+
   function onConnect(c: Connection) {
     const { nodes, edges } = get();
     set({
@@ -447,64 +439,66 @@
         });
       }
     }
-    if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
-      const sourceEnvVars = nodeEnvVarNames(sn);
-      if (sourceEnvVars.length === 0) {
-        throw new Error("MUST NOT REACH!");
+    if (tn.type === "app") {
+      if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
+        const sourceEnvVars = nodeEnvVarNames(sn);
+        if (sourceEnvVars.length === 0) {
+          throw new Error("MUST NOT REACH!");
+        }
+        const id = uuidv4();
+        if (sourceEnvVars.length === 1) {
+          updateNode<"app">(c.target, {
+            ...tn,
+            data: {
+              ...tn.data,
+              envVars: [
+                ...(tn.data.envVars || []),
+                {
+                  id: id,
+                  source: c.source,
+                  name: sourceEnvVars[0],
+                  isEditting: false,
+                },
+              ],
+            },
+          });
+        } else {
+          updateNode<"app">(c.target, {
+            ...tn,
+            data: {
+              ...tn.data,
+              envVars: [
+                ...(tn.data.envVars || []),
+                {
+                  id: id,
+                  source: c.source,
+                },
+              ],
+            },
+          });
+        }
       }
-      const id = uuidv4();
-      if (sourceEnvVars.length === 1) {
-        updateNode(c.target, {
-          ...tn,
-          data: {
-            ...tn.data,
-            envVars: [
-              ...(tn.data.envVars || []),
-              {
-                id: id,
-                source: c.source,
-                name: sourceEnvVars[0],
-                isEditting: false,
-              },
-            ],
-          },
-        });
-      } else {
-        updateNode(c.target, {
-          ...tn,
-          data: {
-            ...tn.data,
-            envVars: [
-              ...(tn.data.envVars || []),
-              {
-                id: id,
-                source: c.source,
-              },
-            ],
-          },
-        });
-      }
-    }
-    if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
-      const sourcePorts = sn.data.ports || [];
-      const id = uuidv4();
-      if (sourcePorts.length === 1) {
-        updateNode(c.target, {
-          ...tn,
-          data: {
-            ...tn.data,
-            envVars: [
-              ...(tn.data.envVars || []),
-              {
-                id: id,
-                source: c.source,
-                name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
-                portId: sourcePorts[0].id,
-                isEditting: false,
-              },
-            ],
-          },
-        });
+      if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
+        const sourcePorts = sn.data.ports || [];
+        const id = uuidv4();
+        if (sourcePorts.length === 1) {
+          updateNode<"app">(c.target, {
+            ...tn,
+            data: {
+              ...tn.data,
+              envVars: [
+                ...(tn.data.envVars || []),
+                {
+                  id: id,
+                  source: c.source,
+                  name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
+                  portId: sourcePorts[0].id,
+                  isEditting: false,
+                },
+              ],
+            },
+          });
+        }
       }
     }
     if (c.sourceHandle === "volume") {
@@ -580,6 +574,8 @@
     edges: [],
     categories: defaultCategories,
     messages: v([]),
+    env: defaultEnv,
+    githubService: null,
     setHighlightCategory: (name, active) => {
       set({
         categories: get().categories.map(
@@ -639,15 +635,41 @@
     updateNodeData,
     onConnect,
     refreshEnv: async () => {
-      return get().env;
-      const resp = await fetch("/env");
-      if (!resp.ok) {
-        throw new Error("failed to fetch env config");
+      const projectId = get().projectId;
+      let env: Env = defaultEnv;
+
+      try {
+        if (projectId) {
+          const response = await fetch(`/api/project/${projectId}/env`);
+          if (response.ok) {
+            const data = await response.json();
+            const result = envSchema.safeParse(data);
+            if (result.success) {
+              env = result.data;
+            } else {
+              console.error("Invalid env data:", result.error);
+            }
+          }
+        }
+      } catch (error) {
+        console.error("Failed to fetch integrations:", error);
+      } finally {
+        set({ env: env });
+        if (env.integrations.github) {
+          set({ githubService: new GitHubServiceImpl(projectId!) });
+        } else {
+          set({ githubService: null });
+        }
       }
-      set({ env: envSchema.parse(await resp.json()) });
-      return get().env;
     },
-    setProject: (projectId) => set({ projectId }),
+    setProject: (projectId) => {
+      set({
+        projectId,
+      });
+      if (projectId) {
+        get().refreshEnv();
+      }
+    },
     setProjects: (projects) => set({ projects }),
   };
 });