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({