blob: 8ada938ef7e0280145b8237458606294ac7963be [file] [log] [blame]
gio5f2f1002025-03-20 18:38:48 +04001import { Category, defaultCategories } from "./categories";
2import { CreateValidators, Validator } from "./config";
gioa71316d2025-05-24 09:41:36 +04003import { GitHubService, GitHubServiceImpl, GitHubRepository } from "./github";
gioc31bf142025-06-16 07:48:20 +00004import type { Edge, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
gioaf8db832025-05-13 14:43:05 +00005import {
6 addEdge,
7 applyEdgeChanges,
8 applyNodeChanges,
9 Connection,
10 EdgeChange,
11 useNodes,
12 XYPosition,
13} from "@xyflow/react";
giod0026612025-05-08 13:00:36 +000014import type { DeepPartial } from "react-hook-form";
15import { v4 as uuidv4 } from "uuid";
giod0026612025-05-08 13:00:36 +000016import { create } from "zustand";
gioc31bf142025-06-16 07:48:20 +000017import { AppNode, Env, NodeType, VolumeNode, GatewayTCPData, envSchema } from "config";
gio5f2f1002025-03-20 18:38:48 +040018
19export function nodeLabel(n: AppNode): string {
gio48fde052025-05-14 09:48:08 +000020 try {
21 switch (n.type) {
22 case "network":
23 return n.data.domain;
24 case "app":
25 return n.data.label || "Service";
26 case "github":
27 return n.data.repository?.fullName || "Github";
28 case "gateway-https": {
gio29050d62025-05-16 04:49:26 +000029 if (n.data && n.data.subdomain) {
30 return `${n.data.subdomain}`;
gio48fde052025-05-14 09:48:08 +000031 } else {
32 return "HTTPS Gateway";
33 }
giod0026612025-05-08 13:00:36 +000034 }
gio48fde052025-05-14 09:48:08 +000035 case "gateway-tcp": {
gio29050d62025-05-16 04:49:26 +000036 if (n.data && n.data.subdomain) {
37 return `${n.data.subdomain}`;
gio48fde052025-05-14 09:48:08 +000038 } else {
39 return "TCP Gateway";
40 }
giod0026612025-05-08 13:00:36 +000041 }
gio48fde052025-05-14 09:48:08 +000042 case "mongodb":
43 return n.data.label || "MongoDB";
44 case "postgresql":
45 return n.data.label || "PostgreSQL";
46 case "volume":
47 return n.data.label || "Volume";
48 case undefined:
49 throw new Error("MUST NOT REACH!");
giod0026612025-05-08 13:00:36 +000050 }
gio48fde052025-05-14 09:48:08 +000051 } catch (e) {
52 console.error("opaa", e);
53 } finally {
54 console.log("done");
giod0026612025-05-08 13:00:36 +000055 }
gioa1efbad2025-05-21 07:16:45 +000056 return "Unknown Node";
gio5f2f1002025-03-20 18:38:48 +040057}
58
gio8fad76a2025-05-22 14:01:23 +000059export function nodeLabelFull(n: AppNode): string {
60 if (n.type === "gateway-https") {
61 return `https://${n.data.subdomain}.${n.data.network}`;
62 } else {
63 return nodeLabel(n);
64 }
65}
66
gio5f2f1002025-03-20 18:38:48 +040067export function nodeIsConnectable(n: AppNode, handle: string): boolean {
giod0026612025-05-08 13:00:36 +000068 switch (n.type) {
69 case "network":
70 return true;
71 case "app":
72 if (handle === "ports") {
73 return n.data !== undefined && n.data.ports !== undefined && n.data.ports.length > 0;
74 } else if (handle === "repository") {
75 if (!n.data || !n.data.repository || !n.data.repository.id) {
76 return true;
77 }
78 return false;
79 }
80 return false;
81 case "github":
82 if (n.data.repository?.id !== undefined) {
83 return true;
84 }
85 return false;
86 case "gateway-https":
87 if (handle === "subdomain") {
88 return n.data.network === undefined;
89 }
90 return n.data === undefined || n.data.https === undefined;
91 case "gateway-tcp":
92 if (handle === "subdomain") {
93 return n.data.network === undefined;
94 }
95 return true;
96 case "mongodb":
97 return true;
98 case "postgresql":
99 return true;
100 case "volume":
101 if (n.data === undefined || n.data.type === undefined) {
102 return false;
103 }
104 if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
105 return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
106 }
107 return true;
108 case undefined:
109 throw new Error("MUST NOT REACH!");
110 }
gio5f2f1002025-03-20 18:38:48 +0400111}
112
giob41ecae2025-04-24 08:46:50 +0000113export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
giod0026612025-05-08 13:00:36 +0000114 return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
giob41ecae2025-04-24 08:46:50 +0000115}
116
gio5f2f1002025-03-20 18:38:48 +0400117export function nodeEnvVarNames(n: AppNode): string[] {
giod0026612025-05-08 13:00:36 +0000118 switch (n.type) {
119 case "app":
120 return [
121 `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
122 ...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
123 ];
124 case "github":
125 return [];
126 case "gateway-https":
127 return [];
128 case "gateway-tcp":
129 return [];
130 case "mongodb":
131 return [`DODO_MONGODB_${n.data.label.toUpperCase()}_URL`];
132 case "postgresql":
133 return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
134 case "volume":
135 return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
136 case undefined:
137 throw new Error("MUST NOT REACH");
138 default:
139 throw new Error("MUST NOT REACH");
140 }
gio5f2f1002025-03-20 18:38:48 +0400141}
142
gio5f2f1002025-03-20 18:38:48 +0400143export type MessageType = "INFO" | "WARNING" | "FATAL";
144
145export type Message = {
giod0026612025-05-08 13:00:36 +0000146 id: string;
147 type: MessageType;
148 nodeId?: string;
149 message: string;
150 onHighlight?: (state: AppState) => void;
151 onLooseHighlight?: (state: AppState) => void;
152 onClick?: (state: AppState) => void;
gio5f2f1002025-03-20 18:38:48 +0400153};
154
gio7f98e772025-05-07 11:00:14 +0000155const defaultEnv: Env = {
gio7d813702025-05-08 18:29:52 +0000156 managerAddr: undefined,
gioa71316d2025-05-24 09:41:36 +0400157 deployKeyPublic: undefined,
158 instanceId: undefined,
giod0026612025-05-08 13:00:36 +0000159 networks: [],
160 integrations: {
161 github: false,
162 },
gio3a921b82025-05-10 07:36:09 +0000163 services: [],
gio3ed59592025-05-14 16:51:09 +0000164 user: {
165 id: "",
166 username: "",
167 },
giob77cb932025-05-19 09:37:14 +0000168 access: [],
gio7f98e772025-05-07 11:00:14 +0000169};
170
gio5f2f1002025-03-20 18:38:48 +0400171export type Project = {
giod0026612025-05-08 13:00:36 +0000172 id: string;
173 name: string;
174};
gio5f2f1002025-03-20 18:38:48 +0400175
gio7f98e772025-05-07 11:00:14 +0000176export type IntegrationsConfig = {
giod0026612025-05-08 13:00:36 +0000177 github: boolean;
gio7f98e772025-05-07 11:00:14 +0000178};
179
180type NodeUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>>;
181type NodeDataUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>["data"]>;
182
gioaf8db832025-05-13 14:43:05 +0000183type Viewport = {
184 transformX: number;
185 transformY: number;
186 transformZoom: number;
187 width: number;
188 height: number;
189};
190
gio918780d2025-05-22 08:24:41 +0000191let refreshEnvIntervalId: number | null = null;
192
gio5f2f1002025-03-20 18:38:48 +0400193export type AppState = {
giod0026612025-05-08 13:00:36 +0000194 projectId: string | undefined;
gio818da4e2025-05-12 14:45:35 +0000195 mode: "edit" | "deploy";
giod0026612025-05-08 13:00:36 +0000196 projects: Project[];
197 nodes: AppNode[];
198 edges: Edge[];
gio359a6852025-05-14 03:38:24 +0000199 zoom: ReactFlowViewport;
giod0026612025-05-08 13:00:36 +0000200 categories: Category[];
201 messages: Message[];
202 env: Env;
gioaf8db832025-05-13 14:43:05 +0000203 viewport: Viewport;
204 setViewport: (viewport: Viewport) => void;
giod0026612025-05-08 13:00:36 +0000205 githubService: GitHubService | null;
gioa71316d2025-05-24 09:41:36 +0400206 githubRepositories: GitHubRepository[];
207 githubRepositoriesLoading: boolean;
208 githubRepositoriesError: string | null;
giod0026612025-05-08 13:00:36 +0000209 setHighlightCategory: (name: string, active: boolean) => void;
210 onNodesChange: OnNodesChange<AppNode>;
211 onEdgesChange: OnEdgesChange;
212 onConnect: OnConnect;
gioaf8db832025-05-13 14:43:05 +0000213 addNode: (node: Omit<AppNode, "position">) => void;
giod0026612025-05-08 13:00:36 +0000214 setNodes: (nodes: AppNode[]) => void;
215 setEdges: (edges: Edge[]) => void;
gio818da4e2025-05-12 14:45:35 +0000216 setProject: (projectId: string | undefined) => Promise<void>;
217 setMode: (mode: "edit" | "deploy") => void;
giod0026612025-05-08 13:00:36 +0000218 updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
219 updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
220 replaceEdge: (c: Connection, id?: string) => void;
221 refreshEnv: () => Promise<void>;
gioa71316d2025-05-24 09:41:36 +0400222 fetchGithubRepositories: () => Promise<void>;
gio5f2f1002025-03-20 18:38:48 +0400223};
224
225const projectIdSelector = (state: AppState) => state.projectId;
226const categoriesSelector = (state: AppState) => state.categories;
227const messagesSelector = (state: AppState) => state.messages;
gio7f98e772025-05-07 11:00:14 +0000228const githubServiceSelector = (state: AppState) => state.githubService;
gio5f2f1002025-03-20 18:38:48 +0400229const envSelector = (state: AppState) => state.env;
gio359a6852025-05-14 03:38:24 +0000230const zoomSelector = (state: AppState) => state.zoom;
gioa71316d2025-05-24 09:41:36 +0400231const githubRepositoriesSelector = (state: AppState) => state.githubRepositories;
232const githubRepositoriesLoadingSelector = (state: AppState) => state.githubRepositoriesLoading;
233const githubRepositoriesErrorSelector = (state: AppState) => state.githubRepositoriesError;
gioaf8db832025-05-13 14:43:05 +0000234
gio359a6852025-05-14 03:38:24 +0000235export function useZoom(): ReactFlowViewport {
236 return useStateStore(zoomSelector);
gioaf8db832025-05-13 14:43:05 +0000237}
gio5f2f1002025-03-20 18:38:48 +0400238
239export function useProjectId(): string | undefined {
giod0026612025-05-08 13:00:36 +0000240 return useStateStore(projectIdSelector);
gio5f2f1002025-03-20 18:38:48 +0400241}
242
giob45b1862025-05-20 11:42:20 +0000243export function useSetProject(): (projectId: string | undefined) => void {
244 return useStateStore((state) => state.setProject);
245}
246
gio5f2f1002025-03-20 18:38:48 +0400247export function useCategories(): Category[] {
giod0026612025-05-08 13:00:36 +0000248 return useStateStore(categoriesSelector);
gio5f2f1002025-03-20 18:38:48 +0400249}
250
251export function useMessages(): Message[] {
giod0026612025-05-08 13:00:36 +0000252 return useStateStore(messagesSelector);
gio5f2f1002025-03-20 18:38:48 +0400253}
254
255export function useNodeMessages(id: string): Message[] {
giod0026612025-05-08 13:00:36 +0000256 return useMessages().filter((m) => m.nodeId === id);
gio5f2f1002025-03-20 18:38:48 +0400257}
258
259export function useNodeLabel(id: string): string {
giod0026612025-05-08 13:00:36 +0000260 return nodeLabel(useNodes<AppNode>().find((n) => n.id === id)!);
gio5f2f1002025-03-20 18:38:48 +0400261}
262
263export function useNodePortName(id: string, portId: string): string {
giod0026612025-05-08 13:00:36 +0000264 return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
gio5f2f1002025-03-20 18:38:48 +0400265}
266
gio5f2f1002025-03-20 18:38:48 +0400267export function useEnv(): Env {
giod0026612025-05-08 13:00:36 +0000268 return useStateStore(envSelector);
gio7f98e772025-05-07 11:00:14 +0000269}
270
271export function useGithubService(): GitHubService | null {
giod0026612025-05-08 13:00:36 +0000272 return useStateStore(githubServiceSelector);
gio5f2f1002025-03-20 18:38:48 +0400273}
274
gioa71316d2025-05-24 09:41:36 +0400275export function useGithubRepositories(): GitHubRepository[] {
276 return useStateStore(githubRepositoriesSelector);
277}
278
279export function useGithubRepositoriesLoading(): boolean {
280 return useStateStore(githubRepositoriesLoadingSelector);
281}
282
283export function useGithubRepositoriesError(): string | null {
284 return useStateStore(githubRepositoriesErrorSelector);
285}
286
287export function useFetchGithubRepositories(): () => Promise<void> {
288 return useStateStore((state) => state.fetchGithubRepositories);
289}
290
gio3ec94242025-05-16 12:46:57 +0000291export function useMode(): "edit" | "deploy" {
292 return useStateStore((state) => state.mode);
293}
294
gio5f2f1002025-03-20 18:38:48 +0400295const v: Validator = CreateValidators();
296
gioaf8db832025-05-13 14:43:05 +0000297function getRandomPosition({ width, height, transformX, transformY, transformZoom }: Viewport): XYPosition {
298 const zoomMultiplier = 1 / transformZoom;
299 const realWidth = width * zoomMultiplier;
300 const realHeight = height * zoomMultiplier;
301 const paddingMultiplier = 0.8;
302 const ret = {
303 x: -transformX * zoomMultiplier + Math.random() * realWidth * paddingMultiplier,
304 y: -transformY * zoomMultiplier + Math.random() * realHeight * paddingMultiplier,
305 };
306 return ret;
307}
308
gio3d0bf032025-06-05 06:57:26 +0000309export const useStateStore = create<AppState>((setOg, get): AppState => {
310 const set = (state: Partial<AppState>) => {
311 setOg(state);
312 };
giod0026612025-05-08 13:00:36 +0000313 const setN = (nodes: AppNode[]) => {
gio4b9b58a2025-05-12 11:46:08 +0000314 set({
giod0026612025-05-08 13:00:36 +0000315 nodes,
gio5cf364c2025-05-08 16:01:21 +0000316 messages: v(nodes),
gio4b9b58a2025-05-12 11:46:08 +0000317 });
318 };
319
gio918780d2025-05-22 08:24:41 +0000320 const startRefreshEnvInterval = () => {
321 if (refreshEnvIntervalId) {
322 clearInterval(refreshEnvIntervalId);
323 }
324 if (get().projectId && typeof document !== "undefined" && document.visibilityState === "visible") {
325 console.log("Starting refreshEnv interval for project:", get().projectId);
326 refreshEnvIntervalId = setInterval(async () => {
327 if (get().projectId && typeof document !== "undefined" && document.visibilityState === "visible") {
328 console.log("Interval: Calling refreshEnv for project:", get().projectId);
329 await get().refreshEnv();
330 } else if (refreshEnvIntervalId) {
331 console.log(
332 "Interval: Conditions not met (project removed or tab hidden), stopping interval from inside.",
333 );
334 clearInterval(refreshEnvIntervalId);
335 refreshEnvIntervalId = null;
336 }
337 }, 5000) as unknown as number;
338 } else {
339 console.log(
340 "Not starting refreshEnv interval. Project ID:",
341 get().projectId,
342 "Visibility:",
343 typeof document !== "undefined" ? document.visibilityState : "SSR",
344 );
345 }
346 };
347
348 const stopRefreshEnvInterval = () => {
349 if (refreshEnvIntervalId) {
350 console.log("Stopping refreshEnv interval for project:", get().projectId);
351 clearInterval(refreshEnvIntervalId);
352 refreshEnvIntervalId = null;
353 }
354 };
355
356 if (typeof document !== "undefined") {
357 document.addEventListener("visibilitychange", () => {
358 if (document.visibilityState === "visible") {
359 console.log("Tab became visible, attempting to start refreshEnv interval.");
360 startRefreshEnvInterval();
361 } else {
362 console.log("Tab became hidden, stopping refreshEnv interval.");
363 stopRefreshEnvInterval();
364 }
365 });
366 }
367
gio48fde052025-05-14 09:48:08 +0000368 const injectNetworkNodes = () => {
369 const newNetworks = get().env.networks.filter(
370 (x) => !get().nodes.some((n) => n.type === "network" && n.data.domain === x.domain),
371 );
372 newNetworks.forEach((n) => {
373 get().addNode({
374 id: n.domain,
375 type: "network",
376 connectable: true,
377 data: {
378 domain: n.domain,
379 label: n.domain,
380 envVars: [],
381 ports: [],
382 state: "success", // TODO(gio): monitor network health
383 },
384 });
385 console.log("added network", n.domain);
386 });
387 };
388
gio4b9b58a2025-05-12 11:46:08 +0000389 const restoreSaved = async () => {
gio818da4e2025-05-12 14:45:35 +0000390 const { projectId } = get();
391 const resp = await fetch(`/api/project/${projectId}/saved/${get().mode === "deploy" ? "deploy" : "draft"}`, {
gio4b9b58a2025-05-12 11:46:08 +0000392 method: "GET",
393 });
394 const inst = await resp.json();
gioc31bf142025-06-16 07:48:20 +0000395 setN(inst.state.nodes);
396 set({ edges: inst.state.edges });
gio48fde052025-05-14 09:48:08 +0000397 injectNetworkNodes();
gio359a6852025-05-14 03:38:24 +0000398 if (
gioc31bf142025-06-16 07:48:20 +0000399 get().zoom.x !== inst.state.viewport.x ||
400 get().zoom.y !== inst.state.viewport.y ||
401 get().zoom.zoom !== inst.state.viewport.zoom
gio359a6852025-05-14 03:38:24 +0000402 ) {
gioc31bf142025-06-16 07:48:20 +0000403 set({ zoom: inst.state.viewport });
gio359a6852025-05-14 03:38:24 +0000404 }
giod0026612025-05-08 13:00:36 +0000405 };
gio7f98e772025-05-07 11:00:14 +0000406
giod0026612025-05-08 13:00:36 +0000407 function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
408 setN(
409 get().nodes.map((n) => {
410 if (n.id === id) {
411 return {
412 ...n,
413 data: {
414 ...n.data,
415 ...data,
416 },
417 } as Extract<AppNode, { type: T }>;
418 }
419 return n;
420 }),
421 );
422 }
gio7f98e772025-05-07 11:00:14 +0000423
giod0026612025-05-08 13:00:36 +0000424 function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
425 setN(
426 get().nodes.map((n) => {
427 if (n.id === id) {
428 return {
429 ...n,
430 ...node,
431 } as Extract<AppNode, { type: T }>;
432 }
433 return n;
434 }),
435 );
436 }
gio7f98e772025-05-07 11:00:14 +0000437
giod0026612025-05-08 13:00:36 +0000438 function onConnect(c: Connection) {
439 const { nodes, edges } = get();
440 set({
441 edges: addEdge(c, edges),
442 });
443 const sn = nodes.filter((n) => n.id === c.source)[0]!;
444 const tn = nodes.filter((n) => n.id === c.target)[0]!;
445 if (tn.type === "network") {
446 if (sn.type === "gateway-https") {
447 updateNodeData<"gateway-https">(sn.id, {
448 network: tn.data.domain,
449 });
450 } else if (sn.type === "gateway-tcp") {
451 updateNodeData<"gateway-tcp">(sn.id, {
452 network: tn.data.domain,
453 });
454 }
455 }
456 if (tn.type === "app") {
457 if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
458 const sourceEnvVars = nodeEnvVarNames(sn);
459 if (sourceEnvVars.length === 0) {
460 throw new Error("MUST NOT REACH!");
461 }
462 const id = uuidv4();
463 if (sourceEnvVars.length === 1) {
464 updateNode<"app">(c.target, {
465 ...tn,
466 data: {
467 ...tn.data,
468 envVars: [
469 ...(tn.data.envVars || []),
470 {
471 id: id,
472 source: c.source,
473 name: sourceEnvVars[0],
474 isEditting: false,
475 },
476 ],
477 },
478 });
479 } else {
480 updateNode<"app">(c.target, {
481 ...tn,
482 data: {
483 ...tn.data,
484 envVars: [
485 ...(tn.data.envVars || []),
486 {
487 id: id,
488 source: c.source,
489 },
490 ],
491 },
492 });
493 }
494 }
495 if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
496 const sourcePorts = sn.data.ports || [];
497 const id = uuidv4();
498 if (sourcePorts.length === 1) {
499 updateNode<"app">(c.target, {
500 ...tn,
501 data: {
502 ...tn.data,
503 envVars: [
504 ...(tn.data.envVars || []),
505 {
506 id: id,
507 source: c.source,
508 name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
509 portId: sourcePorts[0].id,
510 isEditting: false,
511 },
512 ],
513 },
514 });
515 }
516 }
gio3d0bf032025-06-05 06:57:26 +0000517 if (c.targetHandle === "repository") {
518 const sourceNode = nodes.find((n) => n.id === c.source);
519 if (sourceNode && sourceNode.type === "github" && sourceNode.data.repository) {
520 updateNodeData<"app">(tn.id, {
521 repository: {
522 id: sourceNode.data.repository.id,
523 repoNodeId: c.source,
524 },
525 });
526 }
527 }
giod0026612025-05-08 13:00:36 +0000528 }
529 if (c.sourceHandle === "volume") {
530 updateNodeData<"volume">(c.source, {
531 attachedTo: ((sn as VolumeNode).data.attachedTo || []).concat(c.source),
532 });
533 }
534 if (c.targetHandle === "volume") {
535 if (tn.type === "postgresql" || tn.type === "mongodb") {
536 updateNodeData(c.target, {
537 volumeId: c.source,
538 });
539 }
540 }
541 if (c.targetHandle === "https") {
542 if ((sn.data.ports || []).length === 1) {
543 updateNodeData<"gateway-https">(c.target, {
544 https: {
545 serviceId: c.source,
546 portId: sn.data.ports![0].id,
547 },
548 });
549 } else {
550 updateNodeData<"gateway-https">(c.target, {
551 https: {
552 serviceId: c.source,
553 portId: "", // TODO(gio)
554 },
555 });
556 }
557 }
558 if (c.targetHandle === "tcp") {
559 const td = tn.data as GatewayTCPData;
560 if ((sn.data.ports || []).length === 1) {
561 updateNodeData<"gateway-tcp">(c.target, {
562 exposed: (td.exposed || []).concat({
563 serviceId: c.source,
564 portId: sn.data.ports![0].id,
565 }),
566 });
567 } else {
568 updateNodeData<"gateway-tcp">(c.target, {
569 selected: {
570 serviceId: c.source,
571 portId: undefined,
572 },
573 });
574 }
575 }
576 if (sn.type === "app") {
577 if (c.sourceHandle === "ports") {
578 updateNodeData<"app">(sn.id, {
579 isChoosingPortToConnect: true,
580 });
581 }
582 }
giod0026612025-05-08 13:00:36 +0000583 }
gioa71316d2025-05-24 09:41:36 +0400584
585 const fetchGithubRepositories = async () => {
586 const { githubService, projectId } = get();
587 if (!githubService || !projectId) {
588 set({
589 githubRepositories: [],
590 githubRepositoriesError: "GitHub service or Project ID not available.",
591 githubRepositoriesLoading: false,
592 });
593 return;
594 }
595
596 set({ githubRepositoriesLoading: true, githubRepositoriesError: null });
597 try {
598 const repos = await githubService.getRepositories();
599 set({ githubRepositories: repos, githubRepositoriesLoading: false });
600 } catch (error) {
601 console.error("Failed to fetch GitHub repositories in store:", error);
602 const errorMessage = error instanceof Error ? error.message : "Unknown error fetching repositories";
603 set({ githubRepositories: [], githubRepositoriesError: errorMessage, githubRepositoriesLoading: false });
604 }
605 };
606
giod0026612025-05-08 13:00:36 +0000607 return {
608 projectId: undefined,
gio818da4e2025-05-12 14:45:35 +0000609 mode: "edit",
giod0026612025-05-08 13:00:36 +0000610 projects: [],
611 nodes: [],
612 edges: [],
613 categories: defaultCategories,
614 messages: v([]),
615 env: defaultEnv,
gioaf8db832025-05-13 14:43:05 +0000616 viewport: {
617 transformX: 0,
618 transformY: 0,
619 transformZoom: 1,
620 width: 800,
621 height: 600,
622 },
gio359a6852025-05-14 03:38:24 +0000623 zoom: {
624 x: 0,
625 y: 0,
626 zoom: 1,
627 },
giod0026612025-05-08 13:00:36 +0000628 githubService: null,
gioa71316d2025-05-24 09:41:36 +0400629 githubRepositories: [],
630 githubRepositoriesLoading: false,
631 githubRepositoriesError: null,
gioaf8db832025-05-13 14:43:05 +0000632 setViewport: (viewport) => {
633 const { viewport: vp } = get();
634 if (
635 viewport.transformX !== vp.transformX ||
636 viewport.transformY !== vp.transformY ||
637 viewport.transformZoom !== vp.transformZoom ||
638 viewport.width !== vp.width ||
639 viewport.height !== vp.height
640 ) {
641 set({ viewport });
642 }
643 },
giod0026612025-05-08 13:00:36 +0000644 setHighlightCategory: (name, active) => {
645 set({
646 categories: get().categories.map((c) => {
647 if (c.title.toLowerCase() !== name.toLowerCase()) {
648 return c;
649 } else {
650 return {
651 ...c,
652 active,
653 };
654 }
655 }),
656 });
657 },
658 onNodesChange: (changes) => {
659 const nodes = applyNodeChanges(changes, get().nodes);
660 setN(nodes);
661 },
662 onEdgesChange: (changes) => {
663 set({
664 edges: applyEdgeChanges(changes, get().edges),
665 });
666 },
gioaf8db832025-05-13 14:43:05 +0000667 addNode: (node) => {
668 const { viewport, nodes } = get();
669 setN(
670 nodes.concat({
671 ...node,
672 position: getRandomPosition(viewport),
gioa1efbad2025-05-21 07:16:45 +0000673 } as AppNode),
gioaf8db832025-05-13 14:43:05 +0000674 );
675 },
giod0026612025-05-08 13:00:36 +0000676 setNodes: (nodes) => {
677 setN(nodes);
678 },
679 setEdges: (edges) => {
680 set({ edges });
681 },
682 replaceEdge: (c, id) => {
683 let change: EdgeChange;
684 if (id === undefined) {
685 change = {
686 type: "add",
687 item: {
688 id: uuidv4(),
689 ...c,
690 },
691 };
692 onConnect(c);
693 } else {
694 change = {
695 type: "replace",
696 id,
697 item: {
698 id,
699 ...c,
700 },
701 };
702 }
703 set({
704 edges: applyEdgeChanges([change], get().edges),
705 });
706 },
707 updateNode,
708 updateNodeData,
709 onConnect,
710 refreshEnv: async () => {
711 const projectId = get().projectId;
712 let env: Env = defaultEnv;
giod0026612025-05-08 13:00:36 +0000713 try {
714 if (projectId) {
715 const response = await fetch(`/api/project/${projectId}/env`);
716 if (response.ok) {
717 const data = await response.json();
718 const result = envSchema.safeParse(data);
719 if (result.success) {
720 env = result.data;
721 } else {
722 console.error("Invalid env data:", result.error);
723 }
724 }
725 }
726 } catch (error) {
727 console.error("Failed to fetch integrations:", error);
728 } finally {
gioa71316d2025-05-24 09:41:36 +0400729 const oldEnv = get().env;
730 const oldGithubIntegrationStatus = oldEnv.integrations.github;
731 if (JSON.stringify(oldEnv) !== JSON.stringify(env)) {
gio4b9b58a2025-05-12 11:46:08 +0000732 set({ env });
gio48fde052025-05-14 09:48:08 +0000733 injectNetworkNodes();
gioa71316d2025-05-24 09:41:36 +0400734 let ghService = null;
gio4b9b58a2025-05-12 11:46:08 +0000735 if (env.integrations.github) {
gioa71316d2025-05-24 09:41:36 +0400736 ghService = new GitHubServiceImpl(projectId!);
737 }
738 if (get().githubService !== ghService || (ghService && !get().githubService)) {
739 set({ githubService: ghService });
740 }
741 if (
742 ghService &&
743 (oldGithubIntegrationStatus !== env.integrations.github || !oldEnv.integrations.github)
744 ) {
745 get().fetchGithubRepositories();
746 }
747 if (!env.integrations.github) {
748 set({
749 githubRepositories: [],
750 githubRepositoriesError: null,
751 githubRepositoriesLoading: false,
752 });
gio4b9b58a2025-05-12 11:46:08 +0000753 }
giod0026612025-05-08 13:00:36 +0000754 }
755 }
756 },
gio818da4e2025-05-12 14:45:35 +0000757 setMode: (mode) => {
758 set({ mode });
759 },
760 setProject: async (projectId) => {
gio918780d2025-05-22 08:24:41 +0000761 const currentProjectId = get().projectId;
762 if (projectId === currentProjectId) {
gio359a6852025-05-14 03:38:24 +0000763 return;
764 }
gio918780d2025-05-22 08:24:41 +0000765 stopRefreshEnvInterval();
giod0026612025-05-08 13:00:36 +0000766 set({
767 projectId,
gioa71316d2025-05-24 09:41:36 +0400768 githubRepositories: [],
769 githubRepositoriesLoading: false,
770 githubRepositoriesError: null,
giod0026612025-05-08 13:00:36 +0000771 });
772 if (projectId) {
gio818da4e2025-05-12 14:45:35 +0000773 await get().refreshEnv();
gioa71316d2025-05-24 09:41:36 +0400774 if (get().env.instanceId) {
gio818da4e2025-05-12 14:45:35 +0000775 set({ mode: "deploy" });
776 } else {
777 set({ mode: "edit" });
778 }
gio4b9b58a2025-05-12 11:46:08 +0000779 restoreSaved();
gio918780d2025-05-22 08:24:41 +0000780 startRefreshEnvInterval();
gio4b9b58a2025-05-12 11:46:08 +0000781 } else {
782 set({
783 nodes: [],
784 edges: [],
gio918780d2025-05-22 08:24:41 +0000785 env: defaultEnv,
786 githubService: null,
gioa71316d2025-05-24 09:41:36 +0400787 githubRepositories: [],
788 githubRepositoriesLoading: false,
789 githubRepositoriesError: null,
gio4b9b58a2025-05-12 11:46:08 +0000790 });
giod0026612025-05-08 13:00:36 +0000791 }
792 },
gioa71316d2025-05-24 09:41:36 +0400793 fetchGithubRepositories: fetchGithubRepositories,
giod0026612025-05-08 13:00:36 +0000794 };
gio5f2f1002025-03-20 18:38:48 +0400795});