blob: f292c3059f34f7068e908eaa9845298a003718da [file] [log] [blame]
import { AppNode, nodeLabel, useEnv, useMessages, useProjectId, useStateStore } from "@/lib/state";
import { Button } from "./ui/button";
import { useCallback, useEffect, useState } from "react";
import { generateDodoConfig } from "@/lib/config";
import { useNodes, useReactFlow } from "@xyflow/react";
import { useToast } from "@/hooks/use-toast";
import {
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { Menu } from "lucide-react";
function toNodeType(t: string): string {
if (t === "ingress") {
return "gateway-https";
} else if (t === "service") {
return "app";
} else {
return t;
}
}
export function Actions() {
const { toast } = useToast();
const store = useStateStore();
const projectId = useProjectId();
const nodes = useNodes<AppNode>();
const env = useEnv();
const messages = useMessages();
const instance = useReactFlow();
const [ok, setOk] = useState(false);
const [loading, setLoading] = useState(false);
const [reloading, setReloading] = useState(false);
useEffect(() => {
setOk(!messages.some((m) => m.type === "FATAL"));
}, [messages, setOk]);
const monitor = useCallback(async () => {
const m = async function () {
const resp = await fetch(`/api/project/${projectId}/status`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (resp.status !== 200) {
return;
}
const data: { type: string; name: string; status: string }[] = await resp.json();
console.log(data);
for (const n of nodes) {
if (n.type === "network") {
continue;
}
const d = data.find((d) => n.type === toNodeType(d.type) && nodeLabel(n) === d.name);
if (d !== undefined) {
store.updateNodeData(n.id, {
state: d?.status,
});
}
}
if (data.find((d) => d.status !== "success" && d.status != "failure") !== undefined) {
setTimeout(m, 1000);
}
};
setTimeout(m, 100);
}, [projectId, nodes, store]);
const deploy = useCallback(async () => {
if (projectId == null) {
return;
}
setLoading(true);
store.setMode("deploy");
try {
const config = generateDodoConfig(projectId, nodes, env);
if (config == null) {
throw new Error("MUST NOT REACH!");
}
const resp = await fetch(`/api/project/${projectId}/deploy`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
state: JSON.stringify(instance.toObject()),
config,
}),
});
if (resp.ok) {
toast({
title: "Deployment succeeded",
});
monitor();
} else {
toast({
variant: "destructive",
title: "Deployment failed",
description: await resp.text(),
});
}
} catch (e) {
store.setMode("edit");
console.log(e);
toast({
variant: "destructive",
title: "Deployment failed",
});
} finally {
setLoading(false);
}
}, [projectId, instance, nodes, env, setLoading, toast, monitor, store]);
const save = useCallback(async () => {
if (projectId == null) {
return;
}
const resp = await fetch(`/api/project/${projectId}/saved`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(instance.toObject()),
});
if (resp.ok) {
toast({
title: "Save succeeded",
});
} else {
toast({
variant: "destructive",
title: "Save failed",
description: await resp.text(),
});
}
}, [projectId, instance, toast]);
const restoreSaved = useCallback(async () => {
if (projectId == null) {
return;
}
const resp = await fetch(`/api/project/${projectId}/saved/${store.mode === "deploy" ? "deploy" : "draft"}`, {
method: "GET",
});
const inst = await resp.json();
const { x = 0, y = 0, zoom = 1 } = inst.viewport;
store.setNodes(inst.nodes || []);
store.setEdges(inst.edges || []);
instance.setViewport({ x, y, zoom });
}, [projectId, instance, store]);
const clear = useCallback(() => {
store.setEdges([]);
store.setNodes([]);
instance.setViewport({ x: 0, y: 0, zoom: 1 });
}, [store, instance]);
const edit = useCallback(async () => {
store.setMode("edit");
}, [store]);
const deleteProject = useCallback(async () => {
if (projectId == null) {
return;
}
const resp = await fetch(`/api/project/${projectId}`, {
method: "DELETE",
});
if (resp.ok) {
clear();
store.setProject(undefined);
toast({
title: "Project deleted",
});
} else {
toast({
variant: "destructive",
title: "Failed to delete project",
description: await resp.text(),
});
}
}, [store, clear, projectId, toast]);
const reload = useCallback(async () => {
if (projectId == null) {
return;
}
setReloading(true);
try {
const resp = await fetch(`/api/project/${projectId}/reload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (resp.ok) {
toast({
title: "Reload triggered successfully",
});
} else {
toast({
variant: "destructive",
title: "Reload failed",
description: await resp.text(),
});
}
} catch (e) {
console.log(e);
toast({
variant: "destructive",
title: "Reload failed",
});
} finally {
setReloading(false);
}
}, [projectId, toast]);
const [deployProps, setDeployProps] = useState({});
const [reloadProps, setReloadProps] = useState({});
useEffect(() => {
if (loading) {
setDeployProps({ loading: true });
} else if (ok) {
setDeployProps({ disabled: false });
} else {
setDeployProps({ disabled: true });
}
if (reloading) {
setReloadProps({ loading: true });
} else {
setReloadProps({ disabled: projectId === undefined });
}
}, [ok, loading, reloading, projectId]);
if (store.mode === "deploy") {
return (
<div className="flex flex-row gap-1 items-center">
<Button onClick={edit} {...reloadProps}>
Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Menu className="rounded-md bg-gray-200 opacity-50" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={reload}
className="cursor-pointer hover:bg-gray-200"
{...reloadProps}
>
Reload Services
</DropdownMenuItem>
<DropdownMenuItem
onClick={deleteProject}
disabled={projectId === undefined}
className="cursor-pointer hover:bg-gray-200"
>
Delete Project
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
} else {
return (
<div className="flex flex-row gap-1 items-center">
<Button onClick={deploy} {...deployProps}>
Deploy
</Button>
<Button onClick={save}>Save</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Menu />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={restoreSaved}
disabled={projectId === undefined}
className="cursor-pointer hover:bg-gray-200"
>
Restore
</DropdownMenuItem>
<DropdownMenuItem onClick={clear} className="cursor-pointer hover:bg-gray-200">
Clear
</DropdownMenuItem>
<DropdownMenuItem
onClick={deleteProject}
disabled={projectId === undefined}
className="cursor-pointer hover:bg-gray-200"
>
Delete Project
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
}