Canvas: Improve layout

Change-Id: Ife4f14d23eefc0ef0cb6b189446590fc42b8d797
diff --git a/apps/canvas/front/src/App.tsx b/apps/canvas/front/src/App.tsx
index afacdb1..f37370a 100644
--- a/apps/canvas/front/src/App.tsx
+++ b/apps/canvas/front/src/App.tsx
@@ -5,38 +5,42 @@
 import { Config } from "./Config";
 import { Integrations } from "./Integrations";
 import { Toaster } from "./components/ui/toaster";
-import { Header } from "./Header";
+import { ProjectSelect } from "./ProjectSelect";
 import { Logs } from "./components/logs";
 
 export default function App() {
 	return (
 		<ReactFlowProvider>
-			<Header />
-			<AppImpl />
-			<Toaster />
+			<div className="h-screen flex flex-col">
+				<AppImpl />
+				<Toaster />
+			</div>
 		</ReactFlowProvider>
 	);
 }
 
 function AppImpl() {
 	return (
-		<Tabs defaultValue="canvas">
-			<TabsList>
-				<TabsTrigger value="canvas">Canvas</TabsTrigger>
-				<TabsTrigger value="config">Config</TabsTrigger>
-				<TabsTrigger value="integrations">Integrations</TabsTrigger>
-				<TabsTrigger value="logs">Logs</TabsTrigger>
-			</TabsList>
-			<TabsContent value="canvas">
+		<Tabs defaultValue="canvas" className="flex-1 flex flex-col min-h-0">
+			<div className="flex items-center justify-between px-4 border-b">
+				<TabsList>
+					<TabsTrigger value="canvas">Canvas</TabsTrigger>
+					<TabsTrigger value="logs">Logs</TabsTrigger>
+					<TabsTrigger value="config">Config</TabsTrigger>
+					<TabsTrigger value="integrations">Integrations</TabsTrigger>
+				</TabsList>
+				<ProjectSelect />
+			</div>
+			<TabsContent value="canvas" className="flex-1 min-h-0">
 				<CanvasBuilder />
 			</TabsContent>
-			<TabsContent value="config">
+			<TabsContent value="config" className="flex-1 min-h-0">
 				<Config />
 			</TabsContent>
-			<TabsContent value="integrations">
+			<TabsContent value="integrations" className="flex-1 min-h-0">
 				<Integrations />
 			</TabsContent>
-			<TabsContent value="logs">
+			<TabsContent value="logs" className="flex-1 min-h-0">
 				<Logs />
 			</TabsContent>
 		</Tabs>
diff --git a/apps/canvas/front/src/Config.tsx b/apps/canvas/front/src/Config.tsx
index df03746..7a93634 100644
--- a/apps/canvas/front/src/Config.tsx
+++ b/apps/canvas/front/src/Config.tsx
@@ -2,6 +2,8 @@
 import { AppNode, useEnv, useProjectId } from "./lib/state";
 import { generateDodoConfig } from "./lib/config";
 import { useEffect, useMemo, useState } from "react";
+import { Card, CardContent } from "./components/ui/card";
+import JSONView from "@microlink/react-json-view";
 
 export function Config() {
 	const env = useEnv();
@@ -14,11 +16,21 @@
 			setNodes(n);
 		}
 	}, [n, setNodes]);
-	const config = useMemo(() => generateDodoConfig(projectId, nodes, env), [projectId, nodes, env]);
-	const configS = useMemo(() => JSON.stringify(config, undefined, 4), [config]);
+	const config = useMemo(() => generateDodoConfig(projectId, nodes, env) || {}, [projectId, nodes, env]);
 	return (
-		<div className="px-5">
-			<pre>{configS}</pre>
-		</div>
+		<Card className="h-full flex flex-col">
+			<CardContent className="flex-1 min-h-0 p-4">
+				<div className="h-full p-4 bg-muted rounded-lg overflow-auto">
+					<JSONView
+						src={config as object}
+						theme="rjv-default"
+						name={false}
+						displayDataTypes={false}
+						enableClipboard={true}
+						style={{ fontFamily: "JetBrains Mono" }}
+					/>
+				</div>
+			</CardContent>
+		</Card>
 	);
 }
diff --git a/apps/canvas/front/src/Header.tsx b/apps/canvas/front/src/ProjectSelect.tsx
similarity index 78%
rename from apps/canvas/front/src/Header.tsx
rename to apps/canvas/front/src/ProjectSelect.tsx
index 979d85c..5178b5e 100644
--- a/apps/canvas/front/src/Header.tsx
+++ b/apps/canvas/front/src/ProjectSelect.tsx
@@ -7,7 +7,7 @@
 import { Dialog, DialogContent, DialogTrigger } from "./components/ui/dialog";
 import { useToast } from "@/hooks/use-toast";
 
-export function Header() {
+export function ProjectSelect() {
 	const { toast } = useToast();
 	const store = useStateStore();
 	const [projects, setProjects] = useState<Project[]>([]);
@@ -96,26 +96,26 @@
 			});
 	}, [name, setCreateNewOpen, toast, store, refreshProjects]);
 	return (
-		<div className="flex flex-row h-9">
-			<Select onValueChange={onSelect} value={project}>
-				<SelectTrigger>
-					<SelectValue placeholder="Choose Project" defaultValue={project} />
-				</SelectTrigger>
-				<SelectContent>
-					{projects.map((p) => (
-						<SelectItem value={p.id}>{p.name}</SelectItem>
-					))}
-					<SelectItem value={"create-new"}>
-						<Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
-							<DialogTrigger>Create New</DialogTrigger>
-							<DialogContent>
-								<Input type="text" placeholder="Name" onChange={updateName} />
-								<Button onClick={createNew}>Create New</Button>
-							</DialogContent>
-						</Dialog>
+		<Select onValueChange={onSelect} value={project}>
+			<SelectTrigger className="w-[200px]">
+				<SelectValue placeholder="Choose Project" defaultValue={project} />
+			</SelectTrigger>
+			<SelectContent>
+				<SelectItem value={"create-new"}>
+					<Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
+						<DialogTrigger>Create New</DialogTrigger>
+						<DialogContent>
+							<Input type="text" placeholder="Name" onChange={updateName} />
+							<Button onClick={createNew}>Create New</Button>
+						</DialogContent>
+					</Dialog>
+				</SelectItem>
+				{projects.map((p) => (
+					<SelectItem key={p.id} value={p.id}>
+						{p.name}
 					</SelectItem>
-				</SelectContent>
-			</Select>
-		</div>
+				))}
+			</SelectContent>
+		</Select>
 	);
 }
diff --git a/apps/canvas/front/src/components/details.tsx b/apps/canvas/front/src/components/details.tsx
index 252952d..cb7d7fe 100644
--- a/apps/canvas/front/src/components/details.tsx
+++ b/apps/canvas/front/src/components/details.tsx
@@ -5,6 +5,7 @@
 import { AccordionItem } from "@radix-ui/react-accordion";
 import { useMemo, useState } from "react";
 import { Icon } from "./icon";
+import { Separator } from "./ui/separator";
 
 function unique<T>(v: T, i: number, a: T[]) {
 	return a.indexOf(v) === i;
@@ -42,18 +43,21 @@
 	const all = useMemo(() => open.concat(selected).filter(unique), [open, selected]);
 	return (
 		<Accordion type="multiple" value={all} onValueChange={(v) => setOpen(v)}>
-			{sorted.map((n) => (
-				<AccordionItem key={n.id} value={n.id} className="px-3">
-					<AccordionTrigger>
-						<div className="flex flex-row space-x-2">
-							{Icon(n.type)}
-							<span>{nodeLabel(n)}</span>
-						</div>
-					</AccordionTrigger>
-					<AccordionContent>
-						<NodeDetails {...n} />
-					</AccordionContent>
-				</AccordionItem>
+			{sorted.map((n, index) => (
+				<>
+					{index > 0 && <Separator className="my-2" />}
+					<AccordionItem key={n.id} value={n.id} className="px-3">
+						<AccordionTrigger>
+							<div className="flex flex-row space-x-2">
+								{Icon(n.type)}
+								<span>{nodeLabel(n)}</span>
+							</div>
+						</AccordionTrigger>
+						<AccordionContent>
+							<NodeDetails {...n} />
+						</AccordionContent>
+					</AccordionItem>
+				</>
 			))}
 		</Accordion>
 	);
diff --git a/apps/canvas/front/src/components/logs.tsx b/apps/canvas/front/src/components/logs.tsx
index 5918cf7..a2bf1ba 100644
--- a/apps/canvas/front/src/components/logs.tsx
+++ b/apps/canvas/front/src/components/logs.tsx
@@ -5,6 +5,7 @@
 import { useToast } from "@/hooks/use-toast";
 
 // ANSI escape sequence regex
+// eslint-disable-next-line no-control-regex
 const ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
 
 function cleanAnsiEscapeSequences(text: string): string {
@@ -98,7 +99,7 @@
 	}, [sortedServices, selectedService]);
 
 	return (
-		<Card>
+		<Card className="h-full flex flex-col">
 			<CardHeader>
 				<Select value={selectedService} onValueChange={setSelectedService}>
 					<SelectTrigger>
@@ -113,11 +114,11 @@
 					</SelectContent>
 				</Select>
 			</CardHeader>
-			<CardContent className="h-full">
+			<CardContent className="flex-1 min-h-0">
 				{selectedService && (
 					<pre
 						ref={preRef}
-						className="p-4 bg-muted rounded-lg overflow-auto max-h-[500px] font-['JetBrains_Mono'] whitespace-pre-wrap break-all"
+						className="h-full p-4 bg-muted rounded-lg overflow-auto font-['JetBrains_Mono'] whitespace-pre-wrap break-all"
 					>
 						{cleanAnsiEscapeSequences(logs) || "No logs available"}
 					</pre>