blob: eb89b8a892c40e14100794156067c9f6cb5a3525 [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);
28 useEffect(() => {
29 setOk(!messages.some((m) => m.type === "FATAL"));
30 }, [messages, setOk]);
31 const monitor = useCallback(async () => {
32 const m = async function () {
33 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) {
45 if (n.type === "network") {
46 continue;
47 }
48 const d = data.find((d) => n.type === toNodeType(d.type) && nodeLabel(n) === d.name);
49 if (d !== undefined) {
50 store.updateNodeData(n.id, {
51 state: d?.status,
52 });
53 }
54 }
55 if (data.find((d) => d.status !== "success" && d.status != "failure") !== undefined) {
56 setTimeout(m, 1000);
57 }
58 };
59 setTimeout(m, 100);
60 }, [projectId, nodes, store]);
61 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 });
85 monitor();
86 } else {
87 toast({
88 variant: "destructive",
89 title: "Deployment failed",
90 description: await resp.text(),
91 });
92 }
93 } catch (e) {
94 console.log(e);
95 toast({
96 variant: "destructive",
97 title: "Deployment failed",
98 });
99 } finally {
100 setLoading(false);
101 }
102 }, [projectId, instance, nodes, env, setLoading, toast, monitor]);
103 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 });
124 }
125 }, [projectId, instance, toast]);
126 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 });
138 }, [projectId, instance, store]);
139 const clear = useCallback(() => {
140 store.setEdges([]);
141 store.setNodes([]);
142 instance.setViewport({ x: 0, y: 0, zoom: 1 });
143 }, [store, instance]);
144 // 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 });
164 }
165 }, [store, clear, projectId, toast]);
166 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}>
179 Deploy
180 </Button>
181 <Button onClick={save}>Save</Button>
182 <Button onClick={restoreSaved}>Restore</Button>
183 <Button onClick={clear} variant="destructive">
184 Clear
185 </Button>
186 <Button onClick={deleteProject} variant="destructive" disabled={projectId === undefined}>
187 Delete
188 </Button>
189 </>
190 );
giof8acc612025-04-26 08:20:55 +0400191}