blob: 428ae17d018874f19e7afd2bd94d9168e9e9aaa3 [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;
19 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;
139 };
gio5f2f1002025-03-20 18:38:48 +0400140};
141
142export type GithubNode = Node<GithubData> & {
giod0026612025-05-08 13:00:36 +0000143 type: "github";
gio5f2f1002025-03-20 18:38:48 +0400144};
145
146export type NANode = Node<NodeData> & {
giod0026612025-05-08 13:00:36 +0000147 type: undefined;
gio5f2f1002025-03-20 18:38:48 +0400148};
149
giod0026612025-05-08 13:00:36 +0000150export type AppNode =
151 | NetworkNode
152 | GatewayHttpsNode
153 | GatewayTCPNode
154 | ServiceNode
155 | VolumeNode
156 | PostgreSQLNode
157 | MongoDBNode
158 | GithubNode
159 | NANode;
gio5f2f1002025-03-20 18:38:48 +0400160
161export function nodeLabel(n: AppNode): string {
giod0026612025-05-08 13:00:36 +0000162 switch (n.type) {
163 case "network":
164 return n.data.domain;
165 case "app":
166 return n.data.label || "Service";
167 case "github":
168 return n.data.repository?.sshURL || "Github";
169 case "gateway-https": {
170 if (n.data && n.data.network && n.data.subdomain) {
171 return `https://${n.data.subdomain}.${n.data.network}`;
172 } else {
173 return "HTTPS Gateway";
174 }
175 }
176 case "gateway-tcp": {
177 if (n.data && n.data.network && n.data.subdomain) {
178 return `${n.data.subdomain}.${n.data.network}`;
179 } else {
180 return "TCP Gateway";
181 }
182 }
183 case "mongodb":
184 return n.data.label || "MongoDB";
185 case "postgresql":
186 return n.data.label || "PostgreSQL";
187 case "volume":
188 return n.data.label || "Volume";
189 case undefined:
190 throw new Error("MUST NOT REACH!");
191 }
gio5f2f1002025-03-20 18:38:48 +0400192}
193
194export function nodeIsConnectable(n: AppNode, handle: string): boolean {
giod0026612025-05-08 13:00:36 +0000195 switch (n.type) {
196 case "network":
197 return true;
198 case "app":
199 if (handle === "ports") {
200 return n.data !== undefined && n.data.ports !== undefined && n.data.ports.length > 0;
201 } else if (handle === "repository") {
202 if (!n.data || !n.data.repository || !n.data.repository.id) {
203 return true;
204 }
205 return false;
206 }
207 return false;
208 case "github":
209 if (n.data.repository?.id !== undefined) {
210 return true;
211 }
212 return false;
213 case "gateway-https":
214 if (handle === "subdomain") {
215 return n.data.network === undefined;
216 }
217 return n.data === undefined || n.data.https === undefined;
218 case "gateway-tcp":
219 if (handle === "subdomain") {
220 return n.data.network === undefined;
221 }
222 return true;
223 case "mongodb":
224 return true;
225 case "postgresql":
226 return true;
227 case "volume":
228 if (n.data === undefined || n.data.type === undefined) {
229 return false;
230 }
231 if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
232 return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
233 }
234 return true;
235 case undefined:
236 throw new Error("MUST NOT REACH!");
237 }
gio5f2f1002025-03-20 18:38:48 +0400238}
239
giod0026612025-05-08 13:00:36 +0000240export type BoundEnvVar =
241 | {
242 id: string;
243 source: string | null;
244 }
245 | {
246 id: string;
247 source: string | null;
248 name: string;
249 isEditting: boolean;
250 }
251 | {
252 id: string;
253 source: string | null;
254 name: string;
255 alias: string;
256 isEditting: boolean;
257 }
258 | {
259 id: string;
260 source: string | null;
261 portId: string;
262 name: string;
263 alias: string;
264 isEditting: boolean;
265 };
gio5f2f1002025-03-20 18:38:48 +0400266
267export type EnvVar = {
giod0026612025-05-08 13:00:36 +0000268 name: string;
269 value: string;
gio5f2f1002025-03-20 18:38:48 +0400270};
271
giob41ecae2025-04-24 08:46:50 +0000272export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
giod0026612025-05-08 13:00:36 +0000273 return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
giob41ecae2025-04-24 08:46:50 +0000274}
275
gio5f2f1002025-03-20 18:38:48 +0400276export function nodeEnvVarNames(n: AppNode): string[] {
giod0026612025-05-08 13:00:36 +0000277 switch (n.type) {
278 case "app":
279 return [
280 `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
281 ...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
282 ];
283 case "github":
284 return [];
285 case "gateway-https":
286 return [];
287 case "gateway-tcp":
288 return [];
289 case "mongodb":
290 return [`DODO_MONGODB_${n.data.label.toUpperCase()}_URL`];
291 case "postgresql":
292 return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
293 case "volume":
294 return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
295 case undefined:
296 throw new Error("MUST NOT REACH");
297 default:
298 throw new Error("MUST NOT REACH");
299 }
gio5f2f1002025-03-20 18:38:48 +0400300}
301
302export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
303
304export type MessageType = "INFO" | "WARNING" | "FATAL";
305
306export type Message = {
giod0026612025-05-08 13:00:36 +0000307 id: string;
308 type: MessageType;
309 nodeId?: string;
310 message: string;
311 onHighlight?: (state: AppState) => void;
312 onLooseHighlight?: (state: AppState) => void;
313 onClick?: (state: AppState) => void;
gio5f2f1002025-03-20 18:38:48 +0400314};
315
316export const envSchema = z.object({
gio7d813702025-05-08 18:29:52 +0000317 managerAddr: z.optional(z.string().min(1)),
giod0026612025-05-08 13:00:36 +0000318 deployKey: z.optional(z.string().min(1)),
319 networks: z
320 .array(
321 z.object({
322 name: z.string().min(1),
323 domain: z.string().min(1),
324 }),
325 )
326 .default([]),
327 integrations: z.object({
328 github: z.boolean(),
329 }),
gio3a921b82025-05-10 07:36:09 +0000330 services: z.array(z.string()),
gio5f2f1002025-03-20 18:38:48 +0400331});
332
333export type Env = z.infer<typeof envSchema>;
334
gio7f98e772025-05-07 11:00:14 +0000335const defaultEnv: Env = {
gio7d813702025-05-08 18:29:52 +0000336 managerAddr: undefined,
giod0026612025-05-08 13:00:36 +0000337 deployKey: undefined,
338 networks: [],
339 integrations: {
340 github: false,
341 },
gio3a921b82025-05-10 07:36:09 +0000342 services: [],
gio7f98e772025-05-07 11:00:14 +0000343};
344
gio5f2f1002025-03-20 18:38:48 +0400345export type Project = {
giod0026612025-05-08 13:00:36 +0000346 id: string;
347 name: string;
348};
gio5f2f1002025-03-20 18:38:48 +0400349
gio7f98e772025-05-07 11:00:14 +0000350export type IntegrationsConfig = {
giod0026612025-05-08 13:00:36 +0000351 github: boolean;
gio7f98e772025-05-07 11:00:14 +0000352};
353
354type NodeUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>>;
355type NodeDataUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>["data"]>;
356
gio5f2f1002025-03-20 18:38:48 +0400357export type AppState = {
giod0026612025-05-08 13:00:36 +0000358 projectId: string | undefined;
359 projects: Project[];
360 nodes: AppNode[];
361 edges: Edge[];
362 categories: Category[];
363 messages: Message[];
364 env: Env;
365 githubService: GitHubService | null;
366 setHighlightCategory: (name: string, active: boolean) => void;
367 onNodesChange: OnNodesChange<AppNode>;
368 onEdgesChange: OnEdgesChange;
369 onConnect: OnConnect;
370 setNodes: (nodes: AppNode[]) => void;
371 setEdges: (edges: Edge[]) => void;
372 setProject: (projectId: string | undefined) => void;
373 updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
374 updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
375 replaceEdge: (c: Connection, id?: string) => void;
376 refreshEnv: () => Promise<void>;
gio5f2f1002025-03-20 18:38:48 +0400377};
378
379const projectIdSelector = (state: AppState) => state.projectId;
380const categoriesSelector = (state: AppState) => state.categories;
381const messagesSelector = (state: AppState) => state.messages;
gio7f98e772025-05-07 11:00:14 +0000382const githubServiceSelector = (state: AppState) => state.githubService;
gio5f2f1002025-03-20 18:38:48 +0400383const envSelector = (state: AppState) => state.env;
384
385export function useProjectId(): string | undefined {
giod0026612025-05-08 13:00:36 +0000386 return useStateStore(projectIdSelector);
gio5f2f1002025-03-20 18:38:48 +0400387}
388
389export function useCategories(): Category[] {
giod0026612025-05-08 13:00:36 +0000390 return useStateStore(categoriesSelector);
gio5f2f1002025-03-20 18:38:48 +0400391}
392
393export function useMessages(): Message[] {
giod0026612025-05-08 13:00:36 +0000394 return useStateStore(messagesSelector);
gio5f2f1002025-03-20 18:38:48 +0400395}
396
397export function useNodeMessages(id: string): Message[] {
giod0026612025-05-08 13:00:36 +0000398 return useMessages().filter((m) => m.nodeId === id);
gio5f2f1002025-03-20 18:38:48 +0400399}
400
401export function useNodeLabel(id: string): string {
giod0026612025-05-08 13:00:36 +0000402 return nodeLabel(useNodes<AppNode>().find((n) => n.id === id)!);
gio5f2f1002025-03-20 18:38:48 +0400403}
404
405export function useNodePortName(id: string, portId: string): string {
giod0026612025-05-08 13:00:36 +0000406 return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
gio5f2f1002025-03-20 18:38:48 +0400407}
408
gio5f2f1002025-03-20 18:38:48 +0400409export function useEnv(): Env {
giod0026612025-05-08 13:00:36 +0000410 return useStateStore(envSelector);
gio7f98e772025-05-07 11:00:14 +0000411}
412
413export function useGithubService(): GitHubService | null {
giod0026612025-05-08 13:00:36 +0000414 return useStateStore(githubServiceSelector);
gio5f2f1002025-03-20 18:38:48 +0400415}
416
417const v: Validator = CreateValidators();
418
419export const useStateStore = create<AppState>((set, get): AppState => {
giod0026612025-05-08 13:00:36 +0000420 const setN = (nodes: AppNode[]) => {
gio4b9b58a2025-05-12 11:46:08 +0000421 if (nodes.length == 0) {
422 console.trace("setN", nodes);
423 }
424 set({
giod0026612025-05-08 13:00:36 +0000425 nodes,
gio5cf364c2025-05-08 16:01:21 +0000426 messages: v(nodes),
gio4b9b58a2025-05-12 11:46:08 +0000427 });
428 };
429
430 const restoreSaved = async () => {
431 const resp = await fetch(`/api/project/${get().projectId}/saved`, {
432 method: "GET",
433 });
434 const inst = await resp.json();
435 // const { x = 0, y = 0, zoom = 1 } = inst.viewport;
436 setN(inst.nodes || []);
437 get().setEdges(inst.edges || []);
438 // instance.setViewport({ x, y, zoom });
giod0026612025-05-08 13:00:36 +0000439 };
gio7f98e772025-05-07 11:00:14 +0000440
giod0026612025-05-08 13:00:36 +0000441 function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
442 setN(
443 get().nodes.map((n) => {
444 if (n.id === id) {
445 return {
446 ...n,
447 data: {
448 ...n.data,
449 ...data,
450 },
451 } as Extract<AppNode, { type: T }>;
452 }
453 return n;
454 }),
455 );
456 }
gio7f98e772025-05-07 11:00:14 +0000457
giod0026612025-05-08 13:00:36 +0000458 function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
459 setN(
460 get().nodes.map((n) => {
461 if (n.id === id) {
462 return {
463 ...n,
464 ...node,
465 } as Extract<AppNode, { type: T }>;
466 }
467 return n;
468 }),
469 );
470 }
gio7f98e772025-05-07 11:00:14 +0000471
giod0026612025-05-08 13:00:36 +0000472 function onConnect(c: Connection) {
473 const { nodes, edges } = get();
474 set({
475 edges: addEdge(c, edges),
476 });
477 const sn = nodes.filter((n) => n.id === c.source)[0]!;
478 const tn = nodes.filter((n) => n.id === c.target)[0]!;
479 if (tn.type === "network") {
480 if (sn.type === "gateway-https") {
481 updateNodeData<"gateway-https">(sn.id, {
482 network: tn.data.domain,
483 });
484 } else if (sn.type === "gateway-tcp") {
485 updateNodeData<"gateway-tcp">(sn.id, {
486 network: tn.data.domain,
487 });
488 }
489 }
490 if (tn.type === "app") {
491 if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
492 const sourceEnvVars = nodeEnvVarNames(sn);
493 if (sourceEnvVars.length === 0) {
494 throw new Error("MUST NOT REACH!");
495 }
496 const id = uuidv4();
497 if (sourceEnvVars.length === 1) {
498 updateNode<"app">(c.target, {
499 ...tn,
500 data: {
501 ...tn.data,
502 envVars: [
503 ...(tn.data.envVars || []),
504 {
505 id: id,
506 source: c.source,
507 name: sourceEnvVars[0],
508 isEditting: false,
509 },
510 ],
511 },
512 });
513 } else {
514 updateNode<"app">(c.target, {
515 ...tn,
516 data: {
517 ...tn.data,
518 envVars: [
519 ...(tn.data.envVars || []),
520 {
521 id: id,
522 source: c.source,
523 },
524 ],
525 },
526 });
527 }
528 }
529 if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
530 const sourcePorts = sn.data.ports || [];
531 const id = uuidv4();
532 if (sourcePorts.length === 1) {
533 updateNode<"app">(c.target, {
534 ...tn,
535 data: {
536 ...tn.data,
537 envVars: [
538 ...(tn.data.envVars || []),
539 {
540 id: id,
541 source: c.source,
542 name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
543 portId: sourcePorts[0].id,
544 isEditting: false,
545 },
546 ],
547 },
548 });
549 }
550 }
551 }
552 if (c.sourceHandle === "volume") {
553 updateNodeData<"volume">(c.source, {
554 attachedTo: ((sn as VolumeNode).data.attachedTo || []).concat(c.source),
555 });
556 }
557 if (c.targetHandle === "volume") {
558 if (tn.type === "postgresql" || tn.type === "mongodb") {
559 updateNodeData(c.target, {
560 volumeId: c.source,
561 });
562 }
563 }
564 if (c.targetHandle === "https") {
565 if ((sn.data.ports || []).length === 1) {
566 updateNodeData<"gateway-https">(c.target, {
567 https: {
568 serviceId: c.source,
569 portId: sn.data.ports![0].id,
570 },
571 });
572 } else {
573 updateNodeData<"gateway-https">(c.target, {
574 https: {
575 serviceId: c.source,
576 portId: "", // TODO(gio)
577 },
578 });
579 }
580 }
581 if (c.targetHandle === "tcp") {
582 const td = tn.data as GatewayTCPData;
583 if ((sn.data.ports || []).length === 1) {
584 updateNodeData<"gateway-tcp">(c.target, {
585 exposed: (td.exposed || []).concat({
586 serviceId: c.source,
587 portId: sn.data.ports![0].id,
588 }),
589 });
590 } else {
591 updateNodeData<"gateway-tcp">(c.target, {
592 selected: {
593 serviceId: c.source,
594 portId: undefined,
595 },
596 });
597 }
598 }
599 if (sn.type === "app") {
600 if (c.sourceHandle === "ports") {
601 updateNodeData<"app">(sn.id, {
602 isChoosingPortToConnect: true,
603 });
604 }
605 }
606 if (tn.type === "app") {
607 if (c.targetHandle === "repository") {
608 updateNodeData<"app">(tn.id, {
609 repository: {
610 id: c.source,
611 branch: "master",
612 rootDir: "/",
613 },
614 });
615 }
616 }
617 }
618 return {
619 projectId: undefined,
620 projects: [],
621 nodes: [],
622 edges: [],
623 categories: defaultCategories,
624 messages: v([]),
625 env: defaultEnv,
626 githubService: null,
627 setHighlightCategory: (name, active) => {
628 set({
629 categories: get().categories.map((c) => {
630 if (c.title.toLowerCase() !== name.toLowerCase()) {
631 return c;
632 } else {
633 return {
634 ...c,
635 active,
636 };
637 }
638 }),
639 });
640 },
641 onNodesChange: (changes) => {
642 const nodes = applyNodeChanges(changes, get().nodes);
643 setN(nodes);
644 },
645 onEdgesChange: (changes) => {
646 set({
647 edges: applyEdgeChanges(changes, get().edges),
648 });
649 },
650 setNodes: (nodes) => {
651 setN(nodes);
652 },
653 setEdges: (edges) => {
654 set({ edges });
655 },
656 replaceEdge: (c, id) => {
657 let change: EdgeChange;
658 if (id === undefined) {
659 change = {
660 type: "add",
661 item: {
662 id: uuidv4(),
663 ...c,
664 },
665 };
666 onConnect(c);
667 } else {
668 change = {
669 type: "replace",
670 id,
671 item: {
672 id,
673 ...c,
674 },
675 };
676 }
677 set({
678 edges: applyEdgeChanges([change], get().edges),
679 });
680 },
681 updateNode,
682 updateNodeData,
683 onConnect,
684 refreshEnv: async () => {
685 const projectId = get().projectId;
686 let env: Env = defaultEnv;
gio7f98e772025-05-07 11:00:14 +0000687
giod0026612025-05-08 13:00:36 +0000688 try {
689 if (projectId) {
690 const response = await fetch(`/api/project/${projectId}/env`);
691 if (response.ok) {
692 const data = await response.json();
693 const result = envSchema.safeParse(data);
694 if (result.success) {
695 env = result.data;
696 } else {
697 console.error("Invalid env data:", result.error);
698 }
699 }
700 }
701 } catch (error) {
702 console.error("Failed to fetch integrations:", error);
703 } finally {
gio4b9b58a2025-05-12 11:46:08 +0000704 if (JSON.stringify(get().env) !== JSON.stringify(env)) {
705 set({ env });
706
707 if (env.integrations.github) {
708 set({ githubService: new GitHubServiceImpl(projectId!) });
709 } else {
710 set({ githubService: null });
711 }
giod0026612025-05-08 13:00:36 +0000712 }
713 }
714 },
715 setProject: (projectId) => {
716 set({
717 projectId,
718 });
719 if (projectId) {
720 get().refreshEnv();
gio4b9b58a2025-05-12 11:46:08 +0000721 restoreSaved();
722 } else {
723 set({
724 nodes: [],
725 edges: [],
726 });
giod0026612025-05-08 13:00:36 +0000727 }
728 },
729 };
gio5f2f1002025-03-20 18:38:48 +0400730});