-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement core infrastructure for service graph visualization with custom nodes, layout components, and UI state management. #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,6 @@ dist-ssr | |
| *.njsproj | ||
| *.sln | ||
| *.sw? | ||
|
|
||
|
|
||
| remote/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,67 +1,67 @@ | ||
| # App Graph Builder | ||
|
|
||
| A responsive infrastructure graph editor built with React, TypeScript, | ||
| ReactFlow, shadcn/ui, TanStack Query, Zustand, and Mock Service Worker. | ||
| React Flow, Tailwind CSS, shadcn/ui, TanStack Query, Zustand, and Mock Service | ||
| Worker. | ||
|
|
||
| ## Features | ||
| ## Setup Instructions | ||
|
|
||
| - Screenshot-inspired top bar, icon rail, right panel, and dotted canvas | ||
| - Three-node application graphs with dragging, selection, deletion, zoom, pan, | ||
| fit view, and edge creation | ||
| - Config and Runtime inspector tabs | ||
| - Status badge, editable node fields, and synchronized capacity controls | ||
| - MSW endpoints for `GET /api/apps` and `GET /api/apps/:appId/graph` | ||
| - TanStack Query loading, error, retry, and per-application caching | ||
| - Zustand-managed app selection, node selection, drawer state, and inspector tab | ||
| - shadcn/ui controls and a mobile Sheet drawer | ||
| - Add Node button and keyboard shortcuts | ||
| ### Prerequisites | ||
|
|
||
| ## Setup | ||
| - Node.js `20.19+` or `22.12+` | ||
| - npm | ||
|
|
||
| ### Install and run | ||
|
|
||
| ```bash | ||
| npm install | ||
| npm run dev | ||
| ``` | ||
|
|
||
| Open the local URL printed by Vite, normally `http://localhost:5173`. | ||
| Open the URL printed by Vite, normally `http://localhost:5173`. | ||
|
|
||
| ## Validation | ||
| ### Validate a change | ||
|
|
||
| ```bash | ||
| npm run typecheck | ||
| npm run lint | ||
| npm run build | ||
| ``` | ||
|
|
||
| ## Keyboard Shortcuts | ||
|
|
||
| - `F`: fit the graph into view | ||
| - `P`: toggle the mobile application panel | ||
| - `Delete` or `Backspace`: delete selected graph elements | ||
|
|
||
| ## Key Decisions | ||
|
|
||
| ReactFlow owns mutable node and edge state because its change handlers are | ||
| designed around controlled graph arrays. Zustand stores only cross-component UI | ||
| state and does not duplicate derived node data. | ||
|
|
||
| TanStack Query owns server-like application and graph data. Its query keys | ||
| include the application ID, producing an independent cached graph for each app. | ||
| - React Flow owns the editable node and edge arrays so its drag, connect, and | ||
| delete handlers work with controlled state. | ||
| - Zustand stores cross-component UI state such as application and node | ||
| selection, inspector tabs, panel visibility, and theme. | ||
| - TanStack Query handles application graph loading and caching by application | ||
| ID. | ||
| - MSW provides browser-level mock API endpoints without requiring a backend. | ||
| - The desktop inspector uses a collapsible workspace; smaller screens reuse the | ||
| inspector in a Sheet. Selecting a node opens the relevant panel. | ||
| - Service and database nodes use separate React Flow node components while | ||
| sharing the same inspector data contract. | ||
| - Theme preference is stored in `localStorage`. | ||
|
|
||
| MSW intercepts real browser `fetch` requests. This keeps the API client shaped | ||
| like production code while still requiring no backend. | ||
| ## Known Limitations | ||
|
|
||
| The desktop inspector is an aside. On smaller screens the same panel content is | ||
| rendered in a shadcn Sheet controlled by Zustand. | ||
| - Application and graph data are mocked; no production backend is connected. | ||
| - Node edits, additions, deletions, positions, and new edges are not persisted. | ||
| They reset after a refresh or when graph data is loaded again. | ||
| - Runtime values are illustrative rather than live infrastructure metrics. | ||
| - The Share button is currently a visual placeholder. | ||
| - Authentication, authorization, multi-user collaboration, undo/redo, and | ||
| automated tests are not implemented. | ||
| - The app depends on the MSW service worker starting successfully before React | ||
| is mounted. | ||
|
|
||
| ## Mock Error Demo | ||
| ## Keyboard Shortcuts | ||
|
|
||
| Use the warning button in the top bar to enable or disable mocked HTTP 500 | ||
| responses. Active queries reset so their loading and error states can be tested. | ||
| - `F`: fit the graph into view | ||
| - `P`: toggle the mobile application panel | ||
| - `Delete` or `Backspace`: delete selected graph elements | ||
|
|
||
| ## Known Limitations | ||
| ## Mock Error Demo | ||
|
|
||
| - Mock data and graph edits reset after a full page refresh. | ||
| - Inspector edits are local ReactFlow state and are not submitted to an API. | ||
| - The theme and share buttons are visual placeholders. | ||
| - Authentication and real-time collaboration are not implemented. | ||
| Use the warning button in the top bar to toggle mocked HTTP 500 responses. | ||
| Active queries reset so loading, error, and retry states can be tested. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { Database, HardDrive } from "lucide-react"; | ||
| import { Handle, Position, type NodeProps } from "@xyflow/react"; | ||
|
|
||
| import type { DatabaseGraphNode } from "@/types/graph"; | ||
| import type { MetricProps } from "@/types/components"; | ||
|
|
||
| const statusStyles = { | ||
| healthy: { label: "Healthy", className: "bg-green-500/20 text-green-500" }, | ||
| degraded: { | ||
| label: "Degraded", | ||
| className: "bg-yellow-400/20 text-yellow-500", | ||
| }, | ||
| down: { label: "Down", className: "bg-red-500/20 text-red-500" }, | ||
| } as const; | ||
|
|
||
| function Metric({ | ||
| label, | ||
| value, | ||
| icon: Icon, | ||
| }: MetricProps) { | ||
| return ( | ||
| <div className="grid gap-1"> | ||
| <span className="flex items-center gap-1 text-[11px] text-slate-500 dark:text-slate-400"> | ||
| {Icon && <Icon size={11} />} | ||
| {label} | ||
| </span> | ||
| <strong className="text-xs">{value}</strong> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function DatabaseNode({ | ||
| data, | ||
| selected, | ||
| }: NodeProps<DatabaseGraphNode>) { | ||
| const status = statusStyles[data.status]; | ||
|
|
||
| return ( | ||
| <article | ||
| className={`relative w-[340px] rounded-[18px] border bg-emerald-50 p-5 text-slate-950 shadow-[0_14px_35px_rgb(15_23_42/16%)] dark:bg-[#03110c] dark:text-slate-50 dark:shadow-[0_14px_35px_rgb(0_0_0/35%)] max-[560px]:w-[280px] ${ | ||
| selected | ||
| ? "border-emerald-500 ring-2 ring-emerald-500/25" | ||
| : "border-emerald-200 dark:border-emerald-950" | ||
| }`} | ||
| > | ||
| <Handle | ||
| type="target" | ||
| position={Position.Left} | ||
| className="!size-2.5 !border-2 !border-white !bg-emerald-500 dark:!border-slate-900" | ||
| /> | ||
|
|
||
| <header className="flex items-center justify-between"> | ||
| <div className="flex items-center gap-2.5"> | ||
| <span className="grid size-[34px] place-items-center rounded-[9px] bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"> | ||
| <Database size={18} /> | ||
| </span> | ||
| <div> | ||
| <strong className="block">{data.name}</strong> | ||
| <span className="text-[10px] uppercase tracking-wider text-emerald-600 dark:text-emerald-400"> | ||
| Database | ||
| </span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <span className="grid size-[34px] place-items-center rounded-[9px] bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"> | ||
| <HardDrive size={17} /> | ||
| </span> | ||
| </header> | ||
|
|
||
| <p className="my-4 text-[13px] text-slate-500 dark:text-slate-400"> | ||
| {data.description} | ||
| </p> | ||
|
|
||
| <div className="grid grid-cols-3 gap-2"> | ||
| <Metric label="Capacity" value={`${data.capacity}%`} /> | ||
| <Metric label="Memory" value="0.05 GB" /> | ||
| <Metric label="Storage" value="10 GB" icon={HardDrive} /> | ||
| </div> | ||
|
|
||
| <div className="my-[18px] h-[7px] overflow-hidden rounded-full bg-emerald-100 dark:bg-emerald-950"> | ||
| <span | ||
| className="block h-full rounded-[inherit] bg-gradient-to-r from-emerald-500 to-teal-400" | ||
| style={{ width: `${data.capacity}%` }} | ||
| /> | ||
| </div> | ||
|
|
||
| <footer className="flex items-center justify-between"> | ||
| <span | ||
| className={`rounded-md px-2.5 py-1.5 text-xs font-semibold ${status.className}`} | ||
| > | ||
| {status.label} | ||
| </span> | ||
| <span className="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400"> | ||
| Storage | ||
| </span> | ||
| </footer> | ||
|
|
||
| <Handle | ||
| type="source" | ||
| position={Position.Right} | ||
| className="!size-2.5 !border-2 !border-white !bg-emerald-500 dark:!border-slate-900" | ||
| /> | ||
| </article> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.