blob: dc3503f5360e9cbf0b8ce3d603c8cfe0ffb3205d [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
8export function Actions() {
9 const { toast } = useToast();
10 const store = useStateStore();
11 const projectId = useProjectId();
12 const nodes = useNodes<AppNode>();
13 const env = useEnv();
14 const messages = useMessages();
15 const instance = useReactFlow();
16 const [ok, setOk] = useState(false);
17 const [loading, setLoading] = useState(false);
18 useEffect(() => {
19 setOk(!messages.some((m) => m.type === "FATAL"));
20 }, [messages, setOk]);
gio1dc800a2025-04-24 17:15:43 +000021 const monitor = useCallback(async () => {
giof8acc612025-04-26 08:20:55 +040022 const m = async function () {
gio1dc800a2025-04-24 17:15:43 +000023 const resp = await fetch(`/api/project/${projectId}/status`, {
24 method: "GET",
25 headers: {
26 "Content-Type": "application/json",
27 },
28 })
29 if (resp.status !== 200) {
30 return;
31 }
32 const data: { type: string, name: string, status: string }[] = await resp.json();
33 console.log(data);
34 for (const n of nodes) {
giof8acc612025-04-26 08:20:55 +040035 if (n.type === "network") {
36 continue;
37 }
38 const d = data.find((d) => n.type === d.type && nodeLabel(n) === d.name);
39 if (d !== undefined) {
40 store.updateNodeData(n.id, {
41 state: d?.status,
42 });
gio1dc800a2025-04-24 17:15:43 +000043 }
44 }
giof8acc612025-04-26 08:20:55 +040045 if (data.find((d) => d.status !== "success" && d.status != "failure") !== undefined) {
46 setTimeout(m, 1000);
47 }
gio1dc800a2025-04-24 17:15:43 +000048 };
49 setTimeout(m, 100);
50 }, [projectId, nodes]);
gio5f2f1002025-03-20 18:38:48 +040051 const deploy = useCallback(async () => {
52 if (projectId == null) {
53 return;
54 }
55 setLoading(true);
56 try {
57 const config = generateDodoConfig(nodes, env);
58 if (config == null) {
59 throw new Error("MUST NOT REACH!");
60 }
61 const resp = await fetch(`/api/project/${projectId}/deploy`, {
62 method: "POST",
63 headers: {
64 "Content-Type": "application/json",
65 },
66 body: JSON.stringify({
67 state: JSON.stringify(instance.toObject()),
68 config,
69 }),
70 });
71 if (resp.ok) {
72 toast({
73 title: "Deployment succeeded",
74 });
gio1dc800a2025-04-24 17:15:43 +000075 monitor();
gio5f2f1002025-03-20 18:38:48 +040076 } else {
77 toast({
78 variant: "destructive",
79 title: "Deployment failed",
80 description: await resp.text(),
81 });
giof8acc612025-04-26 08:20:55 +040082 }
gio5f2f1002025-03-20 18:38:48 +040083 } catch (e) {
84 console.log(e);
85 toast({
86 variant: "destructive",
87 title: "Deployment failed",
88 });
89 } finally {
90 setLoading(false);
91 }
92 }, [projectId, instance, nodes, env, setLoading]);
93 const [st, setSt] = useState<string>();
94 const save = useCallback(async () => {
95 if (projectId == null) {
96 return;
97 }
98 const resp = await fetch(`/api/project/${projectId}/saved`, {
99 method: "POST",
100 headers: {
101 "Content-Type": "application/json",
102 },
103 body: JSON.stringify(instance.toObject()),
104 });
105 if (resp.ok) {
106 toast({
107 title: "Save succeeded",
108 });
109 } else {
110 toast({
111 variant: "destructive",
112 title: "Save failed",
113 description: await resp.text(),
114 });
giof8acc612025-04-26 08:20:55 +0400115 }
gio5f2f1002025-03-20 18:38:48 +0400116 }, [projectId, instance, setSt]);
117 const restoreSaved = useCallback(async () => {
118 if (projectId == null) {
119 return;
120 }
121 const resp = await fetch(`/api/project/${projectId}/saved`, {
122 method: "GET",
123 });
124 const inst = await resp.json();
125 const { x = 0, y = 0, zoom = 1 } = inst.viewport;
126 store.setNodes(inst.nodes || []);
127 store.setEdges(inst.edges || []);
128 instance.setViewport({ x, y, zoom });
129 }, [projectId, instance, st]);
gio1dc800a2025-04-24 17:15:43 +0000130 const clear = useCallback(() => {
131 store.setEdges([]);
132 store.setNodes([]);
giob68003c2025-04-25 03:05:21 +0000133 instance.setViewport({ x: 0, y: 0, zoom: 1 });
gio1dc800a2025-04-24 17:15:43 +0000134 }, [store]);
giob68003c2025-04-25 03:05:21 +0000135 // TODO(gio): Update store
136 const deleteProject = useCallback(async () => {
137 if (projectId == null) {
138 return;
139 }
140 const resp = await fetch(`/api/project/${projectId}`, {
141 method: "DELETE",
142 });
143 if (resp.ok) {
144 clear();
145 store.setProject(undefined);
146 toast({
147 title: "Save succeeded",
148 });
149 } else {
150 toast({
151 variant: "destructive",
152 title: "Save failed",
153 description: await resp.text(),
154 });
giof8acc612025-04-26 08:20:55 +0400155 }
giob68003c2025-04-25 03:05:21 +0000156 }, [store, clear]);
gio5f2f1002025-03-20 18:38:48 +0400157 const [props, setProps] = useState({});
158 useEffect(() => {
159 if (loading) {
160 setProps({ loading: true });
161 } else if (ok) {
162 setProps({ disabled: false });
163 } else {
164 setProps({ disabled: true });
165 }
166 }, [ok, loading, setProps]);
167 return (
168 <>
169 <Button onClick={deploy} {...props}>Deploy</Button>
170 <Button onClick={save}>Save</Button>
171 <Button onClick={restoreSaved}>Restore</Button>
giob68003c2025-04-25 03:05:21 +0000172 <Button onClick={clear} variant="destructive">Clear</Button>
173 <Button onClick={deleteProject} variant="destructive" disabled={projectId === undefined}>Delete</Button>
gio5f2f1002025-03-20 18:38:48 +0400174 </>
175 )
giof8acc612025-04-26 08:20:55 +0400176}