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 }),
};
});