Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions napkin-app/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/App.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
7 changes: 7 additions & 0 deletions napkin-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@
"dependencies": {
"@tailwindcss/vite": "^4.2.0",
"@xyflow/react": "^12.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.0"
},
"devDependencies": {
Expand All @@ -26,6 +31,8 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"shadcn": "^3.8.5",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "npm:rolldown-vite@7.2.5"
Expand Down
123 changes: 123 additions & 0 deletions napkin-app/src/App.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";

@custom-variant dark (&:is(.dark *));

#root {
max-width: 1280px;
Expand Down Expand Up @@ -42,3 +46,122 @@
.read-the-docs {
color: #888;
}

@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
148 changes: 103 additions & 45 deletions napkin-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useCallback } from 'react';
import CustomNode from './CustomNode';
import Toolbox from './ToolBox';
import { useState, useCallback } from "react";
import Toolbox from "./ToolBox";
import ResourceNode from "@/components/ResourceNode";
import ResourceEdge from "@/components/ResourceEdge";
import {
ReactFlow,
Background,
Expand All @@ -9,7 +10,6 @@ import {
addEdge,
applyNodeChanges,
applyEdgeChanges,
useReactFlow,
ReactFlowProvider,
type Node,
type Edge,
Expand All @@ -18,67 +18,124 @@ import {
type OnNodesChange,
type OnEdgesChange,
type DefaultEdgeOptions,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const nodeTypes = {
custom: CustomNode,
}

const initialNodes: Node[] = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 5, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 5, y: 100 } },
{ id: '3', type: 'custom', data: { kind: 'text', text: 'Unique' }, position: { x: 5, y: 200 } },
];

const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];

const fitViewOptions: FitViewOptions = {
padding: 0.2,
resource: ResourceNode,
};
const edgeTypes = {
resource: ResourceEdge,
};


const initialNodes: Node[] = [];
const initialEdges: Edge[] = [];

const fitViewOptions: FitViewOptions = { padding: 0.2 };
const defaultEdgeOptions: DefaultEdgeOptions = {
type: "resource",
animated: true,
style: { strokeDasharray: "5 5", stroke: "#888" },
};

function Flow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const { screenToFlowPosition } = useReactFlow();

const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[],
[]
);
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
[]
);
const onConnect: OnConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[],
[]
);

const onAdd = useCallback(
(kind: string) => {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const position = screenToFlowPosition({ x: centerX, y: centerY });

const newNode = {
id: `${Date.now()}`,
type: 'custom',
position,
data:
kind === 'number'
? { kind: 'number', number: 0 }
: { kind: 'text', text: 'Unique' },
};

setNodes((nds) => [...nds, newNode]);
},
[screenToFlowPosition],
);
(kind: string) => {
let spec;

switch (kind) {
case "compute": // EC2 Instance
spec = {
label: "EC2 Instance",
color: "bg-blue-50 border-blue-200",
borderColor: "border-blue-200",
iconColor: "text-blue-400",
inputs: [
{ id: "db-in", type: "data", label: "DB Connection" }, // from Database
{ id: "traffic-in", type: "network", label: "Incoming Traffic" }, // from Load Balancer
],
outputs: [
{ id: "network-out", type: "network", label: "Network" }, // to Security Group / downstream
{ id: "data-out", type: "data", label: "Storage Output" }, // to Storage Bucket
],
};
break;

case "database":
spec = {
label: "Database",
color: "bg-green-50 border-green-200",
borderColor: "border-green-200",
iconColor: "text-green-400",
inputs: [], // no inputs
outputs: [{ id: "db-out", type: "data", label: "DB Output" }], // to EC2
};
break;

case "loadBalancer":
spec = {
label: "Load Balancer",
color: "bg-purple-50 border-purple-200",
borderColor: "border-purple-200",
iconColor: "text-purple-400",
inputs: [{ id: "traffic-in", type: "network", label: "Incoming Traffic" }], // from users / upstream
outputs: [{ id: "traffic-out", type: "network", label: "Forward Traffic" }], // to EC2 nodes
};
break;

case "securityGroup":
spec = {
label: "Security Group",
color: "bg-yellow-50 border-yellow-200",
borderColor: "border-yellow-200",
iconColor: "text-yellow-400",
inputs: [{ id: "inbound", type: "network", label: "Inbound" }], // from EC2 or Load Balancer
outputs: [{ id: "outbound", type: "network", label: "Outbound" }], // to EC2 / network nodes
};
break;

case "storageBucket":
spec = {
label: "Storage Bucket",
color: "bg-orange-50 border-orange-200",
borderColor: "border-orange-200",
iconColor: "text-orange-400",
inputs: [{ id: "data-in", type: "data", label: "Objects" }], // from EC2
outputs: [], // sink
};
break;

default:
return; // unknown kind
}

const newNode: Node = {
id: `${Date.now()}`,
type: "resource",
position: { x: 100, y: 100 }, // fixed starting position
data: { spec },
};

setNodes((nds) => [...nds, newNode]);
},
[]
);

return (
<div className="w-screen h-screen relative">
Expand All @@ -88,6 +145,7 @@ function Flow() {
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
Expand All @@ -102,11 +160,11 @@ function Flow() {
</div>
);
}

export default function App() {
return (
<ReactFlowProvider>
<Flow />
</ReactFlowProvider>
);
}
}
Loading
Loading