blob: b7fe986f074f2262dfaed6e7696793dcf02d1dcb [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { Category, defaultCategories } from "./categories";
2import { CreateValidators, Validator } from "./config";
giod0026612025-05-08 13:00:36 +00003import { GitHubService, GitHubServiceImpl } from "./github";
4import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange } from "@xyflow/react";
5import { addEdge, applyEdgeChanges, applyNodeChanges, Connection, EdgeChange, useNodes } from "@xyflow/react";
6import type { DeepPartial } from "react-hook-form";
7import { v4 as uuidv4 } from "uuid";
gio5f2f1002025-03-20 18:38:48 +04008import { z } from "zod";
giod0026612025-05-08 13:00:36 +00009import { create } from "zustand";
gio5f2f1002025-03-20 18:38:48 +040010
11export type InitData = {
giod0026612025-05-08 13:00:36 +000012 label: string;
13 envVars: BoundEnvVar[];
14 ports: Port[];
gio5f2f1002025-03-20 18:38:48 +040015};
16
17export type NodeData = InitData & {
giod0026612025-05-08 13:00:36 +000018 activeField?: string | undefined;
gio818da4e2025-05-12 14:45:35 +000019 state?: string | null;
gio5f2f1002025-03-20 18:38:48 +040020};
21
22export type PortConnectedTo = {
giod0026612025-05-08 13:00:36 +000023 serviceId: string;
24 portId: string;
25};
gio5f2f1002025-03-20 18:38:48 +040026
gioaba9a962025-04-25 14:19:40 +000027export type NetworkData = NodeData & {
giod0026612025-05-08 13:00:36 +000028 domain: string;
gioaba9a962025-04-25 14:19:40 +000029};
30
31export type NetworkNode = Node<NetworkData> & {
giod0026612025-05-08 13:00:36 +000032 type: "network";
gioaba9a962025-04-25 14:19:40 +000033};
34
gio5f2f1002025-03-20 18:38:48 +040035export type GatewayHttpsData = NodeData & {
giod0026612025-05-08 13:00:36 +000036 network?: string;
37 subdomain?: string;
38 https?: PortConnectedTo;
39 auth?: {
40 enabled: boolean;
41 groups: string[];
42 noAuthPathPatterns: string[];
43 };
gio5f2f1002025-03-20 18:38:48 +040044};
45
46export type GatewayHttpsNode = Node<GatewayHttpsData> & {
giod0026612025-05-08 13:00:36 +000047 type: "gateway-https";
gio5f2f1002025-03-20 18:38:48 +040048};
49
50export type GatewayTCPData = NodeData & {
giod0026612025-05-08 13:00:36 +000051 network?: string;
52 subdomain?: string;
53 exposed: PortConnectedTo[];
54 selected?: {
55 serviceId?: string;
56 portId?: string;
57 };
gio5f2f1002025-03-20 18:38:48 +040058};
59
60export type GatewayTCPNode = Node<GatewayTCPData> & {
giod0026612025-05-08 13:00:36 +000061 type: "gateway-tcp";
gio5f2f1002025-03-20 18:38:48 +040062};
63
64export type Port = {
giod0026612025-05-08 13:00:36 +000065 id: string;
66 name: string;
67 value: number;
gio5f2f1002025-03-20 18:38:48 +040068};
69
gio91165612025-05-03 17:07:38 +000070export const ServiceTypes = [
giod0026612025-05-08 13:00:36 +000071 "deno:2.2.0",
72 "golang:1.20.0",
73 "golang:1.22.0",
74 "golang:1.24.0",
75 "hugo:latest",
76 "php:8.2-apache",
77 "nextjs:deno-2.0.0",
78 "node-23.1.0",
gio91165612025-05-03 17:07:38 +000079] as const;
giod0026612025-05-08 13:00:36 +000080export type ServiceType = (typeof ServiceTypes)[number];
gio5f2f1002025-03-20 18:38:48 +040081
82export type ServiceData = NodeData & {
giod0026612025-05-08 13:00:36 +000083 type: ServiceType;
84 repository:
85 | {
86 id: string;
87 }
88 | {
89 id: string;
90 branch: string;
91 }
92 | {
93 id: string;
94 branch: string;
95 rootDir: string;
96 };
97 env: string[];
98 volume: string[];
99 preBuildCommands: string;
100 isChoosingPortToConnect: boolean;
gio5f2f1002025-03-20 18:38:48 +0400101};
102
103export type ServiceNode = Node<ServiceData> & {
giod0026612025-05-08 13:00:36 +0000104 type: "app";
gio5f2f1002025-03-20 18:38:48 +0400105};
106
107export type VolumeType = "ReadWriteOnce" | "ReadOnlyMany" | "ReadWriteMany" | "ReadWriteOncePod";
108
109export type VolumeData = NodeData & {
giod0026612025-05-08 13:00:36 +0000110 type: VolumeType;
111 size: string;
112 attachedTo: string[];
gio5f2f1002025-03-20 18:38:48 +0400113};
114
115export type VolumeNode = Node<VolumeData> & {
giod0026612025-05-08 13:00:36 +0000116 type: "volume";
gio5f2f1002025-03-20 18:38:48 +0400117};
118
119export type PostgreSQLData = NodeData & {
giod0026612025-05-08 13:00:36 +0000120 volumeId: string;
gio5f2f1002025-03-20 18:38:48 +0400121};
122
123export type PostgreSQLNode = Node<PostgreSQLData> & {
giod0026612025-05-08 13:00:36 +0000124 type: "postgresql";
gio5f2f1002025-03-20 18:38:48 +0400125};
126
127export type MongoDBData = NodeData & {
giod0026612025-05-08 13:00:36 +0000128 volumeId: string;
gio5f2f1002025-03-20 18:38:48 +0400129};
130
131export type MongoDBNode = Node<MongoDBData> & {
giod0026612025-05-08 13:00:36 +0000132 type: "mongodb";
gio5f2f1002025-03-20 18:38:48 +0400133};
134
135export type GithubData = NodeData & {
giod0026612025-05-08 13:00:36 +0000136 repository?: {
137 id: number;
138 sshURL: string;
gio818da4e2025-05-12 14:45:35 +0000139 fullName: string;
giod0026612025-05-08 13:00:36 +0000140 };
gio5f2f1002025-03-20 18:38:48 +0400141};
142
143export type GithubNode = Node<GithubData> & {
giod0026612025-05-08 13:00:36 +0000144 type: "github";
gio5f2f1002025-03-20 18:38:48 +0400145};
146
147export type NANode = Node<NodeData> & {
giod0026612025-05-08 13:00:36 +0000148 type: undefined;
gio5f2f1002025-03-20 18:38:48 +0400149};
150
giod0026612025-05-08 13:00:36 +0000151export type AppNode =
152 | NetworkNode
153 | GatewayHttpsNode
154 | GatewayTCPNode
155 | ServiceNode
156 | VolumeNode
157 | PostgreSQLNode
158 | MongoDBNode
159 | GithubNode
160 | NANode;
gio5f2f1002025-03-20 18:38:48 +0400161
162export function nodeLabel(n: AppNode): string {
giod0026612025-05-08 13:00:36 +0000163 switch (n.type) {
164 case "network":
165 return n.data.domain;
166 case "app":
167 return n.data.label || "Service";
168 case "github":
gio818da4e2025-05-12 14:45:35 +0000169 return n.data.repository?.fullName || "Github";
giod0026612025-05-08 13:00:36 +0000170 case "gateway-https": {
171 if (n.data && n.data.network && n.data.subdomain) {
172 return `https://${n.data.subdomain}.${n.data.network}`;
173 } else {
174 return "HTTPS Gateway";
175 }
176 }
177 case "gateway-tcp": {
178 if (n.data && n.data.network && n.data.subdomain) {
179 return `${n.data.subdomain}.${n.data.network}`;
180 } else {
181 return "TCP Gateway";
182 }
183 }
184 case "mongodb":
185 return n.data.label || "MongoDB";
186 case "postgresql":
187 return n.data.label || "PostgreSQL";
188 case "volume":
189 return n.data.label || "Volume";
190 case undefined:
191 throw new Error("MUST NOT REACH!");
192 }
gio5f2f1002025-03-20 18:38:48 +0400193}
194
195export function nodeIsConnectable(n: AppNode, handle: string): boolean {
giod0026612025-05-08 13:00:36 +0000196 switch (n.type) {
197 case "network":
198 return true;
199 case "app":
200 if (handle === "ports") {
201 return n.data !== undefined && n.data.ports !== undefined && n.data.ports.length > 0;
202 } else if (handle === "repository") {
203 if (!n.data || !n.data.repository || !n.data.repository.id) {
204 return true;
205 }
206 return false;
207 }
208 return false;
209 case "github":
210 if (n.data.repository?.id !== undefined) {
211 return true;
212 }
213 return false;
214 case "gateway-https":
215 if (handle === "subdomain") {
216 return n.data.network === undefined;
217 }
218 return n.data === undefined || n.data.https === undefined;
219 case "gateway-tcp":
220 if (handle === "subdomain") {
221 return n.data.network === undefined;
222 }
223 return true;
224 case "mongodb":
225 return true;
226 case "postgresql":
227 return true;
228 case "volume":
229 if (n.data === undefined || n.data.type === undefined) {
230 return false;
231 }
232 if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
233 return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
234 }
235 return true;
236 case undefined:
237 throw new Error("MUST NOT REACH!");
238 }
gio5f2f1002025-03-20 18:38:48 +0400239}
240
giod0026612025-05-08 13:00:36 +0000241export type BoundEnvVar =
242 | {
243 id: string;
244 source: string | null;
245 }
246 | {
247 id: string;
248 source: string | null;
249 name: string;
250 isEditting: boolean;
251 }
252 | {
253 id: string;
254 source: string | null;
255 name: string;
256 alias: string;
257 isEditting: boolean;
258 }
259 | {
260 id: string;
261 source: string | null;
262 portId: string;
263 name: string;
264 alias: string;
265 isEditting: boolean;
266 };
gio5f2f1002025-03-20 18:38:48 +0400267
268export type EnvVar = {
giod0026612025-05-08 13:00:36 +0000269 name: string;
270 value: string;
gio5f2f1002025-03-20 18:38:48 +0400271};
272
giob41ecae2025-04-24 08:46:50 +0000273export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
giod0026612025-05-08 13:00:36 +0000274 return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
giob41ecae2025-04-24 08:46:50 +0000275}
276
gio5f2f1002025-03-20 18:38:48 +0400277export function nodeEnvVarNames(n: AppNode): string[] {
giod0026612025-05-08 13:00:36 +0000278 switch (n.type) {
279 case "app":
280 return [
281 `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
282 ...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
283 ];
284 case "github":
285 return [];
286 case "gateway-https":
287 return [];
288 case "gateway-tcp":
289 return [];
290 case "mongodb":
291 return [`DODO_MONGODB_${n.data.label.toUpperCase()}_URL`];
292 case "postgresql":
293 return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
294 case "volume":
295 return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
296 case undefined:
297 throw new Error("MUST NOT REACH");
298 default:
299 throw new Error("MUST NOT REACH");
300 }
gio5f2f1002025-03-20 18:38:48 +0400301}
302
303export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
304
305export type MessageType = "INFO" | "WARNING" | "FATAL";
306
307export type Message = {
giod0026612025-05-08 13:00:36 +0000308 id: string;
309 type: MessageType;
310 nodeId?: string;
311 message: string;
312 onHighlight?: (state: AppState) => void;
313 onLooseHighlight?: (state: AppState) => void;
314 onClick?: (state: AppState) => void;
gio5f2f1002025-03-20 18:38:48 +0400315};
316
317export const envSchema = z.object({
gio7d813702025-05-08 18:29:52 +0000318 managerAddr: z.optional(z.string().min(1)),
gio09fcab52025-05-12 14:05:07 +0000319 deployKey: z.optional(z.nullable(z.string().min(1))),
giod0026612025-05-08 13:00:36 +0000320 networks: z
321 .array(
322 z.object({
323 name: z.string().min(1),
324 domain: z.string().min(1),
325 }),
326 )
327 .default([]),
328 integrations: z.object({
329 github: z.boolean(),
330 }),
gio3a921b82025-05-10 07:36:09 +0000331 services: z.array(z.string()),
gio5f2f1002025-03-20 18:38:48 +0400332});
333
334export type Env = z.infer<typeof envSchema>;
335
gio7f98e772025-05-07 11:00:14 +0000336const defaultEnv: Env = {
gio7d813702025-05-08 18:29:52 +0000337 managerAddr: undefined,
giod0026612025-05-08 13:00:36 +0000338 deployKey: undefined,
339 networks: [],
340 integrations: {
341 github: false,
342 },
gio3a921b82025-05-10 07:36:09 +0000343 services: [],
gio7f98e772025-05-07 11:00:14 +0000344};
345
gio5f2f1002025-03-20 18:38:48 +0400346export type Project = {
giod0026612025-05-08 13:00:36 +0000347 id: string;
348 name: string;
349};
gio5f2f1002025-03-20 18:38:48 +0400350
gio7f98e772025-05-07 11:00:14 +0000351export type IntegrationsConfig = {
giod0026612025-05-08 13:00:36 +0000352 github: boolean;
gio7f98e772025-05-07 11:00:14 +0000353};
354
355type NodeUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>>;
356type NodeDataUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>["data"]>;
357
gio5f2f1002025-03-20 18:38:48 +0400358export type AppState = {
giod0026612025-05-08 13:00:36 +0000359 projectId: string | undefined;
gio818da4e2025-05-12 14:45:35 +0000360 mode: "edit" | "deploy";
giod0026612025-05-08 13:00:36 +0000361 projects: Project[];
362 nodes: AppNode[];
363 edges: Edge[];
364 categories: Category[];
365 messages: Message[];
366 env: Env;
367 githubService: GitHubService | null;
368 setHighlightCategory: (name: string, active: boolean) => void;
369 onNodesChange: OnNodesChange<AppNode>;
370 onEdgesChange: OnEdgesChange;
371 onConnect: OnConnect;
372 setNodes: (nodes: AppNode[]) => void;
373 setEdges: (edges: Edge[]) => void;
gio818da4e2025-05-12 14:45:35 +0000374 setProject: (projectId: string | undefined) => Promise<void>;
375 setMode: (mode: "edit" | "deploy") => void;
giod0026612025-05-08 13:00:36 +0000376 updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
377 updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
378 replaceEdge: (c: Connection, id?: string) => void;
379 refreshEnv: () => Promise<void>;
gio5f2f1002025-03-20 18:38:48 +0400380};
381
382const projectIdSelector = (state: AppState) => state.projectId;
383const categoriesSelector = (state: AppState) => state.categories;
384const messagesSelector = (state: AppState) => state.messages;
gio7f98e772025-05-07 11:00:14 +0000385const githubServiceSelector = (state: AppState) => state.githubService;
gio5f2f1002025-03-20 18:38:48 +0400386const envSelector = (state: AppState) => state.env;
387
388export function useProjectId(): string | undefined {
giod0026612025-05-08 13:00:36 +0000389 return useStateStore(projectIdSelector);
gio5f2f1002025-03-20 18:38:48 +0400390}
391
392export function useCategories(): Category[] {
giod0026612025-05-08 13:00:36 +0000393 return useStateStore(categoriesSelector);
gio5f2f1002025-03-20 18:38:48 +0400394}
395
396export function useMessages(): Message[] {
giod0026612025-05-08 13:00:36 +0000397 return useStateStore(messagesSelector);
gio5f2f1002025-03-20 18:38:48 +0400398}
399
400export function useNodeMessages(id: string): Message[] {
giod0026612025-05-08 13:00:36 +0000401 return useMessages().filter((m) => m.nodeId === id);
gio5f2f1002025-03-20 18:38:48 +0400402}
403
404export function useNodeLabel(id: string): string {
giod0026612025-05-08 13:00:36 +0000405 return nodeLabel(useNodes<AppNode>().find((n) => n.id === id)!);
gio5f2f1002025-03-20 18:38:48 +0400406}
407
408export function useNodePortName(id: string, portId: string): string {
giod0026612025-05-08 13:00:36 +0000409 return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
gio5f2f1002025-03-20 18:38:48 +0400410}
411
gio5f2f1002025-03-20 18:38:48 +0400412export function useEnv(): Env {
giod0026612025-05-08 13:00:36 +0000413 return useStateStore(envSelector);
gio7f98e772025-05-07 11:00:14 +0000414}
415
416export function useGithubService(): GitHubService | null {
giod0026612025-05-08 13:00:36 +0000417 return useStateStore(githubServiceSelector);
gio5f2f1002025-03-20 18:38:48 +0400418}
419
420const v: Validator = CreateValidators();
421
422export const useStateStore = create<AppState>((set, get): AppState => {
giod0026612025-05-08 13:00:36 +0000423 const setN = (nodes: AppNode[]) => {
gio4b9b58a2025-05-12 11:46:08 +0000424 if (nodes.length == 0) {
425 console.trace("setN", nodes);
426 }
427 set({
giod0026612025-05-08 13:00:36 +0000428 nodes,
gio5cf364c2025-05-08 16:01:21 +0000429 messages: v(nodes),
gio4b9b58a2025-05-12 11:46:08 +0000430 });
431 };
432
433 const restoreSaved = async () => {
gio818da4e2025-05-12 14:45:35 +0000434 const { projectId } = get();
435 const resp = await fetch(`/api/project/${projectId}/saved/${get().mode === "deploy" ? "deploy" : "draft"}`, {
gio4b9b58a2025-05-12 11:46:08 +0000436 method: "GET",
437 });
438 const inst = await resp.json();
gio4b9b58a2025-05-12 11:46:08 +0000439 setN(inst.nodes || []);
440 get().setEdges(inst.edges || []);
giod0026612025-05-08 13:00:36 +0000441 };
gio7f98e772025-05-07 11:00:14 +0000442
giod0026612025-05-08 13:00:36 +0000443 function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
444 setN(
445 get().nodes.map((n) => {
446 if (n.id === id) {
447 return {
448 ...n,
449 data: {
450 ...n.data,
451 ...data,
452 },
453 } as Extract<AppNode, { type: T }>;
454 }
455 return n;
456 }),
457 );
458 }
gio7f98e772025-05-07 11:00:14 +0000459
giod0026612025-05-08 13:00:36 +0000460 function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
461 setN(
462 get().nodes.map((n) => {
463 if (n.id === id) {
464 return {
465 ...n,
466 ...node,
467 } as Extract<AppNode, { type: T }>;
468 }
469 return n;
470 }),
471 );
472 }
gio7f98e772025-05-07 11:00:14 +0000473
giod0026612025-05-08 13:00:36 +0000474 function onConnect(c: Connection) {
475 const { nodes, edges } = get();
476 set({
477 edges: addEdge(c, edges),
478 });
479 const sn = nodes.filter((n) => n.id === c.source)[0]!;
480 const tn = nodes.filter((n) => n.id === c.target)[0]!;
481 if (tn.type === "network") {
482 if (sn.type === "gateway-https") {
483 updateNodeData<"gateway-https">(sn.id, {
484 network: tn.data.domain,
485 });
486 } else if (sn.type === "gateway-tcp") {
487 updateNodeData<"gateway-tcp">(sn.id, {
488 network: tn.data.domain,
489 });
490 }
491 }
492 if (tn.type === "app") {
493 if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
494 const sourceEnvVars = nodeEnvVarNames(sn);
495 if (sourceEnvVars.length === 0) {
496 throw new Error("MUST NOT REACH!");
497 }
498 const id = uuidv4();
499 if (sourceEnvVars.length === 1) {
500 updateNode<"app">(c.target, {
501 ...tn,
502 data: {
503 ...tn.data,
504 envVars: [
505 ...(tn.data.envVars || []),
506 {
507 id: id,
508 source: c.source,
509 name: sourceEnvVars[0],
510 isEditting: false,
511 },
512 ],
513 },
514 });
515 } else {
516 updateNode<"app">(c.target, {
517 ...tn,
518 data: {
519 ...tn.data,
520 envVars: [
521 ...(tn.data.envVars || []),
522 {
523 id: id,
524 source: c.source,
525 },
526 ],
527 },
528 });
529 }
530 }
531 if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
532 const sourcePorts = sn.data.ports || [];
533 const id = uuidv4();
534 if (sourcePorts.length === 1) {
535 updateNode<"app">(c.target, {
536 ...tn,
537 data: {
538 ...tn.data,
539 envVars: [
540 ...(tn.data.envVars || []),
541 {
542 id: id,
543 source: c.source,
544 name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
545 portId: sourcePorts[0].id,
546 isEditting: false,
547 },
548 ],
549 },
550 });
551 }
552 }
553 }
554 if (c.sourceHandle === "volume") {
555 updateNodeData<"volume">(c.source, {
556 attachedTo: ((sn as VolumeNode).data.attachedTo || []).concat(c.source),
557 });
558 }
559 if (c.targetHandle === "volume") {
560 if (tn.type === "postgresql" || tn.type === "mongodb") {
561 updateNodeData(c.target, {
562 volumeId: c.source,
563 });
564 }
565 }
566 if (c.targetHandle === "https") {
567 if ((sn.data.ports || []).length === 1) {
568 updateNodeData<"gateway-https">(c.target, {
569 https: {
570 serviceId: c.source,
571 portId: sn.data.ports![0].id,
572 },
573 });
574 } else {
575 updateNodeData<"gateway-https">(c.target, {
576 https: {
577 serviceId: c.source,
578 portId: "", // TODO(gio)
579 },
580 });
581 }
582 }
583 if (c.targetHandle === "tcp") {
584 const td = tn.data as GatewayTCPData;
585 if ((sn.data.ports || []).length === 1) {
586 updateNodeData<"gateway-tcp">(c.target, {
587 exposed: (td.exposed || []).concat({
588 serviceId: c.source,
589 portId: sn.data.ports![0].id,
590 }),
591 });
592 } else {
593 updateNodeData<"gateway-tcp">(c.target, {
594 selected: {
595 serviceId: c.source,
596 portId: undefined,
597 },
598 });
599 }
600 }
601 if (sn.type === "app") {
602 if (c.sourceHandle === "ports") {
603 updateNodeData<"app">(sn.id, {
604 isChoosingPortToConnect: true,
605 });
606 }
607 }
608 if (tn.type === "app") {
609 if (c.targetHandle === "repository") {
610 updateNodeData<"app">(tn.id, {
611 repository: {
612 id: c.source,
613 branch: "master",
614 rootDir: "/",
615 },
616 });
617 }
618 }
619 }
620 return {
621 projectId: undefined,
gio818da4e2025-05-12 14:45:35 +0000622 mode: "edit",
giod0026612025-05-08 13:00:36 +0000623 projects: [],
624 nodes: [],
625 edges: [],
626 categories: defaultCategories,
627 messages: v([]),
628 env: defaultEnv,
629 githubService: null,
630 setHighlightCategory: (name, active) => {
631 set({
632 categories: get().categories.map((c) => {
633 if (c.title.toLowerCase() !== name.toLowerCase()) {
634 return c;
635 } else {
636 return {
637 ...c,
638 active,
639 };
640 }
641 }),
642 });
643 },
644 onNodesChange: (changes) => {
645 const nodes = applyNodeChanges(changes, get().nodes);
646 setN(nodes);
647 },
648 onEdgesChange: (changes) => {
649 set({
650 edges: applyEdgeChanges(changes, get().edges),
651 });
652 },
653 setNodes: (nodes) => {
654 setN(nodes);
655 },
656 setEdges: (edges) => {
657 set({ edges });
658 },
659 replaceEdge: (c, id) => {
660 let change: EdgeChange;
661 if (id === undefined) {
662 change = {
663 type: "add",
664 item: {
665 id: uuidv4(),
666 ...c,
667 },
668 };
669 onConnect(c);
670 } else {
671 change = {
672 type: "replace",
673 id,
674 item: {
675 id,
676 ...c,
677 },
678 };
679 }
680 set({
681 edges: applyEdgeChanges([change], get().edges),
682 });
683 },
684 updateNode,
685 updateNodeData,
686 onConnect,
687 refreshEnv: async () => {
688 const projectId = get().projectId;
689 let env: Env = defaultEnv;
gio7f98e772025-05-07 11:00:14 +0000690
giod0026612025-05-08 13:00:36 +0000691 try {
692 if (projectId) {
693 const response = await fetch(`/api/project/${projectId}/env`);
694 if (response.ok) {
695 const data = await response.json();
696 const result = envSchema.safeParse(data);
697 if (result.success) {
698 env = result.data;
699 } else {
700 console.error("Invalid env data:", result.error);
701 }
702 }
703 }
704 } catch (error) {
705 console.error("Failed to fetch integrations:", error);
706 } finally {
gio4b9b58a2025-05-12 11:46:08 +0000707 if (JSON.stringify(get().env) !== JSON.stringify(env)) {
708 set({ env });
709
710 if (env.integrations.github) {
711 set({ githubService: new GitHubServiceImpl(projectId!) });
712 } else {
713 set({ githubService: null });
714 }
giod0026612025-05-08 13:00:36 +0000715 }
716 }
717 },
gio818da4e2025-05-12 14:45:35 +0000718 setMode: (mode) => {
719 set({ mode });
720 },
721 setProject: async (projectId) => {
giod0026612025-05-08 13:00:36 +0000722 set({
723 projectId,
724 });
725 if (projectId) {
gio818da4e2025-05-12 14:45:35 +0000726 await get().refreshEnv();
727 if (get().env.deployKey) {
728 set({ mode: "deploy" });
729 } else {
730 set({ mode: "edit" });
731 }
gio4b9b58a2025-05-12 11:46:08 +0000732 restoreSaved();
733 } else {
734 set({
735 nodes: [],
736 edges: [],
737 });
giod0026612025-05-08 13:00:36 +0000738 }
739 },
740 };
gio5f2f1002025-03-20 18:38:48 +0400741});