blob: 54449c46eba1b87bc26e43418bbc6701cc5acb8c [file] [log] [blame]
gio1dc800a2025-04-24 17:15:43 +00001import { AppNode, nodeLabel, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
gio5f2f1002025-03-20 18:38:48 +04002import { Button } from "./ui/button";
3import { useCallback, useEffect, useState } from "react";
4import { generateDodoConfig } from "@/lib/config";
5import { useNodes, useReactFlow } from "@xyflow/react";
6import { useToast } from "@/hooks/use-toast";
7
gioda708652025-04-30 14:57:38 +04008function toNodeType(t: string): string {
9 if (t === "ingress") {
10 return "gateway-https";
gioa6024622025-05-01 18:37:16 +040011 } else if (t === "service") {
12 return "app";
gioda708652025-04-30 14:57:38 +040013 } else {
14 return t;
15 }
16}
17
gio5f2f1002025-03-20 18:38:48 +040018export function Actions() {
19 const { toast } = useToast();
20 const store = useStateStore();
21 const projectId = useProjectId();
22 const nodes = useNodes<AppNode>();
23 const env = useEnv();
24 const messages = useMessages();
25 const instance = useReactFlow();
26 const [ok, setOk] = useState(false);
27 const [loading, setLoading] = useState(false);
28 useEffect(() => {
29 setOk(!messages.some((m) => m.type === "FATAL"));
30 }, [messages, setOk]);
gio1dc800a2025-04-24 17:15:43 +000031 const monitor = useCallback(async () => {
giof8acc612025-04-26 08:20:55 +040032 const m = async function () {
gio1dc800a2025-04-24 17:15:43 +000033 const resp = await fetch(`/api/project/${projectId}/status`, {
34 method: "GET",
35 headers: {
36 "Content-Type": "application/json",
37 },
38 })
39 if (resp.status !== 200) {
40 return;
41 }
42 const data: { type: string, name: string, status: string }[] = await resp.json();
43 console.log(data);
44 for (const n of nodes) {
giof8acc612025-04-26 08:20:55 +040045 if (n.type === "network") {
46 continue;
47 }
gioda708652025-04-30 14:57:38 +040048 const d = data.find((d) => n.type === toNodeType(d.type) && nodeLabel(n) === d.name);
giof8acc612025-04-26 08:20:55 +040049 if (d !== undefined) {
50 store.updateNodeData(n.id, {
51 state: d?.status,
52 });
gio1dc800a2025-04-24 17:15:43 +000053 }
54 }
giof8acc612025-04-26 08:20:55 +040055 if (data.find((d) => d.status !== "success" && d.status != "failure") !== undefined) {
56 setTimeout(m, 1000);
57 }
gio1dc800a2025-04-24 17:15:43 +000058 };
59 setTimeout(m, 100);
gio6cf8c272025-05-08 09:01:38 +000060 }, [projectId, nodes, store]);
gio5f2f1002025-03-20 18:38:48 +040061 const deploy = useCallback(async () => {
62 if (projectId == null) {
63 return;
64 }
65 setLoading(true);
66 try {
67 const config = generateDodoConfig(nodes, env);
68 if (config == null) {
69 throw new Error("MUST NOT REACH!");
70 }
71 const resp = await fetch(`/api/project/${projectId}/deploy`, {
72 method: "POST",
73 headers: {
74 "Content-Type": "application/json",
75 },
76 body: JSON.stringify({
77 state: JSON.stringify(instance.toObject()),
78 config,
79 }),
80 });
81 if (resp.ok) {
82 toast({
83 title: "Deployment succeeded",
84 });
gio1dc800a2025-04-24 17:15:43 +000085 monitor();
gio5f2f1002025-03-20 18:38:48 +040086 } else {
87 toast({
88 variant: "destructive",
89 title: "Deployment failed",
90 description: await resp.text(),
91 });
giof8acc612025-04-26 08:20:55 +040092 }
gio5f2f1002025-03-20 18:38:48 +040093 } catch (e) {
94 console.log(e);
95 toast({
96 variant: "destructive",
97 title: "Deployment failed",
98 });
99 } finally {
100 setLoading(false);
101 }
gio6cf8c272025-05-08 09:01:38 +0000102 }, [projectId, instance, nodes, env, setLoading, toast, monitor]);
gio5f2f1002025-03-20 18:38:48 +0400103 const save = useCallback(async () => {
104 if (projectId == null) {
105 return;
106 }
107 const resp = await fetch(`/api/project/${projectId}/saved`, {
108 method: "POST",
109 headers: {
110 "Content-Type": "application/json",
111 },
112 body: JSON.stringify(instance.toObject()),
113 });
114 if (resp.ok) {
115 toast({
116 title: "Save succeeded",
117 });
118 } else {
119 toast({
120 variant: "destructive",
121 title: "Save failed",
122 description: await resp.text(),
123 });
giof8acc612025-04-26 08:20:55 +0400124 }
gio6cf8c272025-05-08 09:01:38 +0000125 }, [projectId, instance, toast]);
gio5f2f1002025-03-20 18:38:48 +0400126 const restoreSaved = useCallback(async () => {
127 if (projectId == null) {
128 return;
129 }
130 const resp = await fetch(`/api/project/${projectId}/saved`, {
131 method: "GET",
132 });
133 const inst = await resp.json();
134 const { x = 0, y = 0, zoom = 1 } = inst.viewport;
135 store.setNodes(inst.nodes || []);
136 store.setEdges(inst.edges || []);
137 instance.setViewport({ x, y, zoom });
gio6cf8c272025-05-08 09:01:38 +0000138 }, [projectId, instance, store]);
gio1dc800a2025-04-24 17:15:43 +0000139 const clear = useCallback(() => {
140 store.setEdges([]);
141 store.setNodes([]);
giob68003c2025-04-25 03:05:21 +0000142 instance.setViewport({ x: 0, y: 0, zoom: 1 });
gio6cf8c272025-05-08 09:01:38 +0000143 }, [store, instance]);
giob68003c2025-04-25 03:05:21 +0000144 // TODO(gio): Update store
145 const deleteProject = useCallback(async () => {
146 if (projectId == null) {
147 return;
148 }
149 const resp = await fetch(`/api/project/${projectId}`, {
150 method: "DELETE",
151 });
152 if (resp.ok) {
153 clear();
154 store.setProject(undefined);
155 toast({
156 title: "Save succeeded",
157 });
158 } else {
159 toast({
160 variant: "destructive",
161 title: "Save failed",
162 description: await resp.text(),
163 });
giof8acc612025-04-26 08:20:55 +0400164 }
gio6cf8c272025-05-08 09:01:38 +0000165 }, [store, clear, projectId, toast]);
gio5f2f1002025-03-20 18:38:48 +0400166 const [props, setProps] = useState({});
167 useEffect(() => {
168 if (loading) {
169 setProps({ loading: true });
170 } else if (ok) {
171 setProps({ disabled: false });
172 } else {
173 setProps({ disabled: true });
174 }
175 }, [ok, loading, setProps]);
176 return (
177 <>
178 <Button onClick={deploy} {...props}>Deploy</Button>
179 <Button onClick={save}>Save</Button>
180 <Button onClick={restoreSaved}>Restore</Button>
giob68003c2025-04-25 03:05:21 +0000181 <Button onClick={clear} variant="destructive">Clear</Button>
182 <Button onClick={deleteProject} variant="destructive" disabled={projectId === undefined}>Delete</Button>
gio5f2f1002025-03-20 18:38:48 +0400183 </>
184 )
giof8acc612025-04-26 08:20:55 +0400185}