blob: 58598ca5fc9c79c32e1eaf7346d7a3f2aeb1c79a [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";
11 } else {
12 return t;
13 }
14}
15
gio5f2f1002025-03-20 18:38:48 +040016export function Actions() {
17 const { toast } = useToast();
18 const store = useStateStore();
19 const projectId = useProjectId();
20 const nodes = useNodes<AppNode>();
21 const env = useEnv();
22 const messages = useMessages();
23 const instance = useReactFlow();
24 const [ok, setOk] = useState(false);
25 const [loading, setLoading] = useState(false);
26 useEffect(() => {
27 setOk(!messages.some((m) => m.type === "FATAL"));
28 }, [messages, setOk]);
gio1dc800a2025-04-24 17:15:43 +000029 const monitor = useCallback(async () => {
giof8acc612025-04-26 08:20:55 +040030 const m = async function () {
gio1dc800a2025-04-24 17:15:43 +000031 const resp = await fetch(`/api/project/${projectId}/status`, {
32 method: "GET",
33 headers: {
34 "Content-Type": "application/json",
35 },
36 })
37 if (resp.status !== 200) {
38 return;
39 }
40 const data: { type: string, name: string, status: string }[] = await resp.json();
41 console.log(data);
42 for (const n of nodes) {
giof8acc612025-04-26 08:20:55 +040043 if (n.type === "network") {
44 continue;
45 }
gioda708652025-04-30 14:57:38 +040046 const d = data.find((d) => n.type === toNodeType(d.type) && nodeLabel(n) === d.name);
giof8acc612025-04-26 08:20:55 +040047 if (d !== undefined) {
48 store.updateNodeData(n.id, {
49 state: d?.status,
50 });
gio1dc800a2025-04-24 17:15:43 +000051 }
52 }
giof8acc612025-04-26 08:20:55 +040053 if (data.find((d) => d.status !== "success" && d.status != "failure") !== undefined) {
54 setTimeout(m, 1000);
55 }
gio1dc800a2025-04-24 17:15:43 +000056 };
57 setTimeout(m, 100);
58 }, [projectId, nodes]);
gio5f2f1002025-03-20 18:38:48 +040059 const deploy = useCallback(async () => {
60 if (projectId == null) {
61 return;
62 }
63 setLoading(true);
64 try {
65 const config = generateDodoConfig(nodes, env);
66 if (config == null) {
67 throw new Error("MUST NOT REACH!");
68 }
69 const resp = await fetch(`/api/project/${projectId}/deploy`, {
70 method: "POST",
71 headers: {
72 "Content-Type": "application/json",
73 },
74 body: JSON.stringify({
75 state: JSON.stringify(instance.toObject()),
76 config,
77 }),
78 });
79 if (resp.ok) {
80 toast({
81 title: "Deployment succeeded",
82 });
gio1dc800a2025-04-24 17:15:43 +000083 monitor();
gio5f2f1002025-03-20 18:38:48 +040084 } else {
85 toast({
86 variant: "destructive",
87 title: "Deployment failed",
88 description: await resp.text(),
89 });
giof8acc612025-04-26 08:20:55 +040090 }
gio5f2f1002025-03-20 18:38:48 +040091 } catch (e) {
92 console.log(e);
93 toast({
94 variant: "destructive",
95 title: "Deployment failed",
96 });
97 } finally {
98 setLoading(false);
99 }
100 }, [projectId, instance, nodes, env, setLoading]);
101 const [st, setSt] = useState<string>();
102 const save = useCallback(async () => {
103 if (projectId == null) {
104 return;
105 }
106 const resp = await fetch(`/api/project/${projectId}/saved`, {
107 method: "POST",
108 headers: {
109 "Content-Type": "application/json",
110 },
111 body: JSON.stringify(instance.toObject()),
112 });
113 if (resp.ok) {
114 toast({
115 title: "Save succeeded",
116 });
117 } else {
118 toast({
119 variant: "destructive",
120 title: "Save failed",
121 description: await resp.text(),
122 });
giof8acc612025-04-26 08:20:55 +0400123 }
gio5f2f1002025-03-20 18:38:48 +0400124 }, [projectId, instance, setSt]);
125 const restoreSaved = useCallback(async () => {
126 if (projectId == null) {
127 return;
128 }
129 const resp = await fetch(`/api/project/${projectId}/saved`, {
130 method: "GET",
131 });
132 const inst = await resp.json();
133 const { x = 0, y = 0, zoom = 1 } = inst.viewport;
134 store.setNodes(inst.nodes || []);
135 store.setEdges(inst.edges || []);
136 instance.setViewport({ x, y, zoom });
137 }, [projectId, instance, st]);
gio1dc800a2025-04-24 17:15:43 +0000138 const clear = useCallback(() => {
139 store.setEdges([]);
140 store.setNodes([]);
giob68003c2025-04-25 03:05:21 +0000141 instance.setViewport({ x: 0, y: 0, zoom: 1 });
gio1dc800a2025-04-24 17:15:43 +0000142 }, [store]);
giob68003c2025-04-25 03:05:21 +0000143 // TODO(gio): Update store
144 const deleteProject = useCallback(async () => {
145 if (projectId == null) {
146 return;
147 }
148 const resp = await fetch(`/api/project/${projectId}`, {
149 method: "DELETE",
150 });
151 if (resp.ok) {
152 clear();
153 store.setProject(undefined);
154 toast({
155 title: "Save succeeded",
156 });
157 } else {
158 toast({
159 variant: "destructive",
160 title: "Save failed",
161 description: await resp.text(),
162 });
giof8acc612025-04-26 08:20:55 +0400163 }
giob68003c2025-04-25 03:05:21 +0000164 }, [store, clear]);
gio5f2f1002025-03-20 18:38:48 +0400165 const [props, setProps] = useState({});
166 useEffect(() => {
167 if (loading) {
168 setProps({ loading: true });
169 } else if (ok) {
170 setProps({ disabled: false });
171 } else {
172 setProps({ disabled: true });
173 }
174 }, [ok, loading, setProps]);
175 return (
176 <>
177 <Button onClick={deploy} {...props}>Deploy</Button>
178 <Button onClick={save}>Save</Button>
179 <Button onClick={restoreSaved}>Restore</Button>
giob68003c2025-04-25 03:05:21 +0000180 <Button onClick={clear} variant="destructive">Clear</Button>
181 <Button onClick={deleteProject} variant="destructive" disabled={projectId === undefined}>Delete</Button>
gio5f2f1002025-03-20 18:38:48 +0400182 </>
183 )
giof8acc612025-04-26 08:20:55 +0400184}