blob: 509b51c80c8aa81e4f07dce6c4441464b7d06a35 [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 {
giod0026612025-05-08 13:00:36 +00009 if (t === "ingress") {
10 return "gateway-https";
11 } else if (t === "service") {
12 return "app";
13 } else {
14 return t;
15 }
gioda708652025-04-30 14:57:38 +040016}
17
gio5f2f1002025-03-20 18:38:48 +040018export function Actions() {
giod0026612025-05-08 13:00:36 +000019 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);
gio7d813702025-05-08 18:29:52 +000028 const [reloading, setReloading] = useState(false);
giod0026612025-05-08 13:00:36 +000029 useEffect(() => {
30 setOk(!messages.some((m) => m.type === "FATAL"));
31 }, [messages, setOk]);
32 const monitor = useCallback(async () => {
33 const m = async function () {
34 const resp = await fetch(`/api/project/${projectId}/status`, {
35 method: "GET",
36 headers: {
37 "Content-Type": "application/json",
38 },
39 });
40 if (resp.status !== 200) {
41 return;
42 }
43 const data: { type: string; name: string; status: string }[] = await resp.json();
44 console.log(data);
45 for (const n of nodes) {
46 if (n.type === "network") {
47 continue;
48 }
49 const d = data.find((d) => n.type === toNodeType(d.type) && nodeLabel(n) === d.name);
50 if (d !== undefined) {
51 store.updateNodeData(n.id, {
52 state: d?.status,
53 });
54 }
55 }
56 if (data.find((d) => d.status !== "success" && d.status != "failure") !== undefined) {
57 setTimeout(m, 1000);
58 }
59 };
60 setTimeout(m, 100);
61 }, [projectId, nodes, store]);
62 const deploy = useCallback(async () => {
63 if (projectId == null) {
64 return;
65 }
66 setLoading(true);
67 try {
gio7d813702025-05-08 18:29:52 +000068 const config = generateDodoConfig(projectId, nodes, env);
giod0026612025-05-08 13:00:36 +000069 if (config == null) {
70 throw new Error("MUST NOT REACH!");
71 }
72 const resp = await fetch(`/api/project/${projectId}/deploy`, {
73 method: "POST",
74 headers: {
75 "Content-Type": "application/json",
76 },
77 body: JSON.stringify({
78 state: JSON.stringify(instance.toObject()),
79 config,
80 }),
81 });
82 if (resp.ok) {
83 toast({
84 title: "Deployment succeeded",
85 });
86 monitor();
87 } else {
88 toast({
89 variant: "destructive",
90 title: "Deployment failed",
91 description: await resp.text(),
92 });
93 }
94 } catch (e) {
95 console.log(e);
96 toast({
97 variant: "destructive",
98 title: "Deployment failed",
99 });
100 } finally {
101 setLoading(false);
102 }
103 }, [projectId, instance, nodes, env, setLoading, toast, monitor]);
104 const save = useCallback(async () => {
105 if (projectId == null) {
106 return;
107 }
108 const resp = await fetch(`/api/project/${projectId}/saved`, {
109 method: "POST",
110 headers: {
111 "Content-Type": "application/json",
112 },
113 body: JSON.stringify(instance.toObject()),
114 });
115 if (resp.ok) {
116 toast({
117 title: "Save succeeded",
118 });
119 } else {
120 toast({
121 variant: "destructive",
122 title: "Save failed",
123 description: await resp.text(),
124 });
125 }
126 }, [projectId, instance, toast]);
127 const restoreSaved = useCallback(async () => {
128 if (projectId == null) {
129 return;
130 }
131 const resp = await fetch(`/api/project/${projectId}/saved`, {
132 method: "GET",
133 });
134 const inst = await resp.json();
135 const { x = 0, y = 0, zoom = 1 } = inst.viewport;
136 store.setNodes(inst.nodes || []);
137 store.setEdges(inst.edges || []);
138 instance.setViewport({ x, y, zoom });
139 }, [projectId, instance, store]);
140 const clear = useCallback(() => {
141 store.setEdges([]);
142 store.setNodes([]);
143 instance.setViewport({ x: 0, y: 0, zoom: 1 });
144 }, [store, instance]);
145 // TODO(gio): Update store
146 const deleteProject = useCallback(async () => {
147 if (projectId == null) {
148 return;
149 }
150 const resp = await fetch(`/api/project/${projectId}`, {
151 method: "DELETE",
152 });
153 if (resp.ok) {
154 clear();
155 store.setProject(undefined);
156 toast({
157 title: "Save succeeded",
158 });
159 } else {
160 toast({
161 variant: "destructive",
162 title: "Save failed",
163 description: await resp.text(),
164 });
165 }
166 }, [store, clear, projectId, toast]);
gio7d813702025-05-08 18:29:52 +0000167 const reload = useCallback(async () => {
168 if (projectId == null) {
169 return;
170 }
171 setReloading(true);
172 try {
173 const resp = await fetch(`/api/project/${projectId}/reload`, {
174 method: "POST",
175 headers: {
176 "Content-Type": "application/json",
177 },
178 });
179 if (resp.ok) {
180 toast({
181 title: "Reload triggered successfully",
182 });
183 } else {
184 toast({
185 variant: "destructive",
186 title: "Reload failed",
187 description: await resp.text(),
188 });
189 }
190 } catch (e) {
191 console.log(e);
192 toast({
193 variant: "destructive",
194 title: "Reload failed",
195 });
196 } finally {
197 setReloading(false);
198 }
199 }, [projectId, toast]);
200 const [deployProps, setDeployProps] = useState({});
201 const [reloadProps, setReloadProps] = useState({});
giod0026612025-05-08 13:00:36 +0000202 useEffect(() => {
203 if (loading) {
gio7d813702025-05-08 18:29:52 +0000204 setDeployProps({ loading: true });
giod0026612025-05-08 13:00:36 +0000205 } else if (ok) {
gio7d813702025-05-08 18:29:52 +0000206 setDeployProps({ disabled: false });
giod0026612025-05-08 13:00:36 +0000207 } else {
gio7d813702025-05-08 18:29:52 +0000208 setDeployProps({ disabled: true });
giod0026612025-05-08 13:00:36 +0000209 }
gio7d813702025-05-08 18:29:52 +0000210
211 if (reloading) {
212 setReloadProps({ loading: true });
213 } else {
214 setReloadProps({ disabled: projectId === undefined });
215 }
216 }, [ok, loading, reloading, projectId]);
giod0026612025-05-08 13:00:36 +0000217 return (
218 <>
gio7d813702025-05-08 18:29:52 +0000219 <Button onClick={deploy} {...deployProps}>
giod0026612025-05-08 13:00:36 +0000220 Deploy
221 </Button>
gio7d813702025-05-08 18:29:52 +0000222 <Button onClick={reload} {...reloadProps}>
223 Reload
224 </Button>
giod0026612025-05-08 13:00:36 +0000225 <Button onClick={save}>Save</Button>
226 <Button onClick={restoreSaved}>Restore</Button>
227 <Button onClick={clear} variant="destructive">
228 Clear
229 </Button>
230 <Button onClick={deleteProject} variant="destructive" disabled={projectId === undefined}>
231 Delete
232 </Button>
233 </>
234 );
giof8acc612025-04-26 08:20:55 +0400235}