Canvas: build application infrastructure with drag and drop
Change-Id: I5cfd12e67794f3376c5c025af29470d52d77cf16
diff --git a/apps/canvas/src/components/actions.tsx b/apps/canvas/src/components/actions.tsx
new file mode 100644
index 0000000..cc09a59
--- /dev/null
+++ b/apps/canvas/src/components/actions.tsx
@@ -0,0 +1,116 @@
+import { AppNode, 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";
+
+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);
+ useEffect(() => {
+ setOk(!messages.some((m) => m.type === "FATAL"));
+ }, [messages, setOk]);
+ const deploy = useCallback(async () => {
+ if (projectId == null) {
+ return;
+ }
+ setLoading(true);
+ try {
+ const config = generateDodoConfig(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",
+ });
+ } else {
+ toast({
+ variant: "destructive",
+ title: "Deployment failed",
+ description: await resp.text(),
+ });
+ }
+ } catch (e) {
+ console.log(e);
+ toast({
+ variant: "destructive",
+ title: "Deployment failed",
+ });
+ } finally {
+ setLoading(false);
+ }
+ }, [projectId, instance, nodes, env, setLoading]);
+ const [st, setSt] = useState<string>();
+ 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, setSt]);
+ const restoreSaved = useCallback(async () => {
+ if (projectId == null) {
+ return;
+ }
+ const resp = await fetch(`/api/project/${projectId}/saved`, {
+ 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, st]);
+ const [props, setProps] = useState({});
+ useEffect(() => {
+ if (loading) {
+ setProps({ loading: true });
+ } else if (ok) {
+ setProps({ disabled: false });
+ } else {
+ setProps({ disabled: true });
+ }
+ }, [ok, loading, setProps]);
+ return (
+ <>
+ <Button onClick={deploy} {...props}>Deploy</Button>
+ <Button onClick={save}>Save</Button>
+ <Button onClick={restoreSaved}>Restore</Button>
+ </>
+ )
+}
\ No newline at end of file