Canvas: Edit/Deploy mode
Change-Id: I51e5b6c2a1f06009433b0d0824ffcf3dfe39d34e
diff --git a/apps/canvas/front/src/Canvas.tsx b/apps/canvas/front/src/Canvas.tsx
index c794ade..453176c 100644
--- a/apps/canvas/front/src/Canvas.tsx
+++ b/apps/canvas/front/src/Canvas.tsx
@@ -3,19 +3,25 @@
import { Details } from "@/components/details";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./components/ui/resizable";
import { Tools } from "./Tootls";
+import { useStateStore } from "./lib/state";
export function CanvasBuilder() {
+ const store = useStateStore();
return (
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
<ResizablePanel defaultSize={80}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={80}>
<ResizablePanelGroup direction="horizontal">
- <ResizablePanel defaultSize={15}>
- <Resources />
- </ResizablePanel>
- <ResizableHandle withHandle />
- <ResizablePanel defaultSize={85}>
+ {store.mode === "edit" && (
+ <>
+ <ResizablePanel defaultSize={15}>
+ <Resources />
+ </ResizablePanel>
+ <ResizableHandle withHandle />
+ </>
+ )}
+ <ResizablePanel defaultSize={store.mode === "edit" ? 85 : 100}>
<Canvas />
</ResizablePanel>
</ResizablePanelGroup>
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index 509b51c..f292c30 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -4,6 +4,14 @@
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") {
@@ -64,6 +72,7 @@
return;
}
setLoading(true);
+ store.setMode("deploy");
try {
const config = generateDodoConfig(projectId, nodes, env);
if (config == null) {
@@ -92,6 +101,7 @@
});
}
} catch (e) {
+ store.setMode("edit");
console.log(e);
toast({
variant: "destructive",
@@ -100,7 +110,7 @@
} finally {
setLoading(false);
}
- }, [projectId, instance, nodes, env, setLoading, toast, monitor]);
+ }, [projectId, instance, nodes, env, setLoading, toast, monitor, store]);
const save = useCallback(async () => {
if (projectId == null) {
return;
@@ -128,7 +138,7 @@
if (projectId == null) {
return;
}
- const resp = await fetch(`/api/project/${projectId}/saved`, {
+ const resp = await fetch(`/api/project/${projectId}/saved/${store.mode === "deploy" ? "deploy" : "draft"}`, {
method: "GET",
});
const inst = await resp.json();
@@ -142,7 +152,9 @@
store.setNodes([]);
instance.setViewport({ x: 0, y: 0, zoom: 1 });
}, [store, instance]);
- // TODO(gio): Update store
+ const edit = useCallback(async () => {
+ store.setMode("edit");
+ }, [store]);
const deleteProject = useCallback(async () => {
if (projectId == null) {
return;
@@ -154,12 +166,12 @@
clear();
store.setProject(undefined);
toast({
- title: "Save succeeded",
+ title: "Project deleted",
});
} else {
toast({
variant: "destructive",
- title: "Save failed",
+ title: "Failed to delete project",
description: await resp.text(),
});
}
@@ -214,22 +226,71 @@
setReloadProps({ disabled: projectId === undefined });
}
}, [ok, loading, reloading, projectId]);
- return (
- <>
- <Button onClick={deploy} {...deployProps}>
- Deploy
- </Button>
- <Button onClick={reload} {...reloadProps}>
- Reload
- </Button>
- <Button onClick={save}>Save</Button>
- <Button onClick={restoreSaved}>Restore</Button>
- <Button onClick={clear} variant="destructive">
- Clear
- </Button>
- <Button onClick={deleteProject} variant="destructive" disabled={projectId === undefined}>
- Delete
- </Button>
- </>
- );
+ 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>
+ );
+ }
}
diff --git a/apps/canvas/front/src/components/canvas.tsx b/apps/canvas/front/src/components/canvas.tsx
index 122de3f..8ab4aaa 100644
--- a/apps/canvas/front/src/components/canvas.tsx
+++ b/apps/canvas/front/src/components/canvas.tsx
@@ -123,8 +123,12 @@
fitView
proOptions={{ hideAttribution: true }}
>
- <Controls />
- <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
+ <Controls showInteractive={false} />
+ <Background
+ variant={store.mode === "deploy" ? BackgroundVariant.Dots : BackgroundVariant.Lines}
+ gap={12}
+ size={1}
+ />
<Panel position="bottom-right">
<Actions />
</Panel>
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index cfbbbde..d2aa36e 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -11,6 +11,7 @@
GatewayTCPNode,
GatewayHttpsNode,
AppNode,
+ GithubNode,
} from "@/lib/state";
import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
import { z } from "zod";
@@ -69,7 +70,7 @@
const portSchema = z.object({
name: z.string().min(1, "required"),
- value: z.coerce.number().gt(0, "can not be negative"),
+ value: z.coerce.number().gt(0, "must be positive").lte(65535, "must be less than 65535"),
});
const sourceSchema = z.object({
@@ -103,7 +104,7 @@
store.updateNodeData<"app">(id, {
ports: (data.ports || []).concat({
id: portId,
- name: values.name,
+ name: values.name.toLowerCase(),
value: values.value,
}),
envVars: (data.envVars || []).concat({
@@ -476,7 +477,7 @@
<SelectItem
key={n.id}
value={n.id}
- >{`${n.data.repository?.sshURL}`}</SelectItem>
+ >{`${n.data.repository?.fullName}`}</SelectItem>
))}
</SelectContent>
</Select>
@@ -515,11 +516,13 @@
{data &&
data.ports &&
data.ports.map((p) => (
- <li key={p.id}>
+ <li key={p.id} className="flex flex-row items-center gap-1">
<Button size={"icon"} variant={"ghost"} onClick={() => removePort(p.id)}>
<XIcon />
- </Button>{" "}
- {p.name} - {p.value}
+ </Button>
+ <div>
+ {p.name} - {p.value}
+ </div>
</li>
))}
</ul>
@@ -578,10 +581,12 @@
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
- <Button size={"icon"} variant={"ghost"}>
- <PencilIcon />
- </Button>
- {value}
+ <div className="flex flex-row items-center gap-1">
+ <Button size={"icon"} variant={"ghost"}>
+ <PencilIcon />
+ </Button>
+ <div>{value}</div>
+ </div>
</TooltipTrigger>
<TooltipContent>{v.name}</TooltipContent>
</Tooltip>
diff --git a/apps/canvas/front/src/components/node-gateway-https.tsx b/apps/canvas/front/src/components/node-gateway-https.tsx
index e3f2c42..07eb3c1 100644
--- a/apps/canvas/front/src/components/node-gateway-https.tsx
+++ b/apps/canvas/front/src/components/node-gateway-https.tsx
@@ -390,8 +390,10 @@
name="enabled"
render={({ field }) => (
<FormItem>
- <Checkbox id="authEnabled" onCheckedChange={field.onChange} checked={field.value} />
- <Label htmlFor="authEnabled">Enabled</Label>
+ <div className="flex flex-row gap-1 items-center">
+ <Checkbox id="authEnabled" onCheckedChange={field.onChange} checked={field.value} />
+ <Label htmlFor="authEnabled">Enabled</Label>
+ </div>
<FormMessage />
</FormItem>
)}
@@ -403,11 +405,11 @@
Authorized Groups
<ul>
{(data.auth.groups || []).map((p) => (
- <li key={p}>
+ <li key={p} className="flex flex-row gap-1 items-center">
<Button size={"icon"} variant={"ghost"} onClick={() => removeGroup(p)}>
<XIcon />
- </Button>{" "}
- {p}
+ </Button>
+ <div>{p}</div>
</li>
))}
</ul>
@@ -431,11 +433,11 @@
Auth optional path patterns
<ul>
{(data.auth.noAuthPathPatterns || []).map((p) => (
- <li key={p}>
+ <li key={p} className="flex flex-row gap-1 items-center">
<Button size={"icon"} variant={"ghost"} onClick={() => removeNoAuthPathPattern(p)}>
<XIcon />
- </Button>{" "}
- {p}
+ </Button>
+ <div>{p}</div>
</li>
))}
</ul>
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index 37213b8..ae9d2ab 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -95,6 +95,7 @@
repository: {
id: repo.id,
sshURL: repo.ssh_url,
+ fullName: repo.full_name,
},
});
}
@@ -164,8 +165,7 @@
<Alert variant="destructive" className="mt-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
- GitHub access token is not configured. Please configure it in the
- Integrations tab.
+ Please configure Github Personal Access Token in the Integrations tab.
</AlertDescription>
</Alert>
)}
diff --git a/apps/canvas/front/src/components/node-rect.tsx b/apps/canvas/front/src/components/node-rect.tsx
index 362dc54..99dbd82 100644
--- a/apps/canvas/front/src/components/node-rect.tsx
+++ b/apps/canvas/front/src/components/node-rect.tsx
@@ -7,7 +7,7 @@
selected?: boolean;
children: React.ReactNode;
type: NodeType;
- state: string | null;
+ state?: string | null;
};
export function NodeRect(p: Props) {
diff --git a/apps/canvas/front/src/components/resources.tsx b/apps/canvas/front/src/components/resources.tsx
index 715f878..fbb1be7 100644
--- a/apps/canvas/front/src/components/resources.tsx
+++ b/apps/canvas/front/src/components/resources.tsx
@@ -1,15 +1,14 @@
import { Button } from "@/components/ui/button";
-import { ReactFlowInstance, useReactFlow } from "@xyflow/react";
import { v4 as uuidv4 } from "uuid";
import { useCallback, useState } from "react";
import { Accordion, AccordionTrigger } from "./ui/accordion";
import { AccordionContent, AccordionItem } from "@radix-ui/react-accordion";
-import { NodeType, useCategories } from "@/lib/state";
+import { AppNode, AppState, NodeType, useCategories, useStateStore } from "@/lib/state";
import { CategoryItem } from "@/lib/categories";
import { Icon } from "./icon";
-function addResource(i: CategoryItem<NodeType>, flow: ReactFlowInstance) {
- flow.addNodes({
+function addResource(i: CategoryItem<NodeType>, store: AppState) {
+ const node = {
id: uuidv4(),
position: {
x: 0,
@@ -18,17 +17,24 @@
type: i.type,
connectable: true,
data: i.init,
- });
+ selected: true,
+ };
+ const nodes = store.nodes.map((n) => ({
+ ...n,
+ selected: false,
+ }));
+ nodes.push(node as AppNode);
+ store.setNodes(nodes);
}
export function Resources() {
- const flow = useReactFlow();
+ const store = useStateStore();
const categories = useCategories();
const onResourceAdd = useCallback(
(item: CategoryItem<NodeType>) => {
- return () => addResource(item, flow);
+ return () => addResource(item, store);
},
- [flow],
+ [store],
);
const [open, setOpen] = useState<string[]>(categories.map((c) => c.title));
return (
@@ -39,7 +45,7 @@
<AccordionTrigger>{c.title}</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col space-y-1">
- {c.items.map((item) => (
+ {c.items.map((item: CategoryItem<NodeType>) => (
<Button
key={item.title}
onClick={onResourceAdd(item)}
diff --git a/apps/canvas/front/src/components/ui/dropdown-menu.tsx b/apps/canvas/front/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..e6ac790
--- /dev/null
+++ b/apps/canvas/front/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,179 @@
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { cn } from "@/lib/utils";
+import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+ <DropdownMenuPrimitive.SubTrigger
+ ref={ref}
+ className={cn(
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <ChevronRightIcon className="ml-auto" />
+ </DropdownMenuPrimitive.SubTrigger>
+));
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+>(({ className, ...props }, ref) => (
+ <DropdownMenuPrimitive.SubContent
+ ref={ref}
+ className={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
+ className,
+ )}
+ {...props}
+ />
+));
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => (
+ <DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Content
+ ref={ref}
+ sideOffset={sideOffset}
+ className={cn(
+ "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
+ className,
+ )}
+ {...props}
+ />
+ </DropdownMenuPrimitive.Portal>
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+ <DropdownMenuPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+ inset && "pl-8",
+ className,
+ )}
+ {...props}
+ />
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
+>(({ className, children, checked, ...props }, ref) => (
+ <DropdownMenuPrimitive.CheckboxItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className,
+ )}
+ checked={checked}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <CheckIcon className="h-4 w-4" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.CheckboxItem>
+));
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
+>(({ className, children, ...props }, ref) => (
+ <DropdownMenuPrimitive.RadioItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className,
+ )}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <DotFilledIcon className="h-2 w-2 fill-current" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.RadioItem>
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+ <DropdownMenuPrimitive.Label
+ ref={ref}
+ className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+ {...props}
+ />
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+ <DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
+ return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+};
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 21dbbc4..b7fe986 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -16,7 +16,7 @@
export type NodeData = InitData & {
activeField?: string | undefined;
- state: string | null;
+ state?: string | null;
};
export type PortConnectedTo = {
@@ -136,6 +136,7 @@
repository?: {
id: number;
sshURL: string;
+ fullName: string;
};
};
@@ -165,7 +166,7 @@
case "app":
return n.data.label || "Service";
case "github":
- return n.data.repository?.sshURL || "Github";
+ return n.data.repository?.fullName || "Github";
case "gateway-https": {
if (n.data && n.data.network && n.data.subdomain) {
return `https://${n.data.subdomain}.${n.data.network}`;
@@ -356,6 +357,7 @@
export type AppState = {
projectId: string | undefined;
+ mode: "edit" | "deploy";
projects: Project[];
nodes: AppNode[];
edges: Edge[];
@@ -369,7 +371,8 @@
onConnect: OnConnect;
setNodes: (nodes: AppNode[]) => void;
setEdges: (edges: Edge[]) => void;
- setProject: (projectId: string | undefined) => void;
+ setProject: (projectId: string | undefined) => Promise<void>;
+ setMode: (mode: "edit" | "deploy") => void;
updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
replaceEdge: (c: Connection, id?: string) => void;
@@ -428,14 +431,13 @@
};
const restoreSaved = async () => {
- const resp = await fetch(`/api/project/${get().projectId}/saved`, {
+ const { projectId } = get();
+ const resp = await fetch(`/api/project/${projectId}/saved/${get().mode === "deploy" ? "deploy" : "draft"}`, {
method: "GET",
});
const inst = await resp.json();
- // const { x = 0, y = 0, zoom = 1 } = inst.viewport;
setN(inst.nodes || []);
get().setEdges(inst.edges || []);
- // instance.setViewport({ x, y, zoom });
};
function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
@@ -617,6 +619,7 @@
}
return {
projectId: undefined,
+ mode: "edit",
projects: [],
nodes: [],
edges: [],
@@ -712,12 +715,20 @@
}
}
},
- setProject: (projectId) => {
+ setMode: (mode) => {
+ set({ mode });
+ },
+ setProject: async (projectId) => {
set({
projectId,
});
if (projectId) {
- get().refreshEnv();
+ await get().refreshEnv();
+ if (get().env.deployKey) {
+ set({ mode: "deploy" });
+ } else {
+ set({ mode: "edit" });
+ }
restoreSaved();
} else {
set({