Feat node graph#93
Feat node graph#93Its4Nik wants to merge 18 commits into88-refactor-themesidebarcontext-to-separate-ui-state-remove-themeprops-bag-and-avoid-no-op-defaultsfrom
Conversation
…h DockNodes and containers
This commit introduces a comprehensive update to the infrastructure graph visualization, expanding it to include DockNodes and individual Docker containers. This enhancement provides a more complete overview of the monitored environment by integrating all layers of infrastructure into a single, interactive graph.
**Key changes include:**
* **API Enhancements (`apps/api`):**
* Introduced new `calculateNodeLayout` and `reachableStatus` helpers to dynamically arrange and standardize the status of all node types (clients, hosts, docknodes, containers).
* Added robust `GraphModel` schemas for API request/response validation, including definitions for new node types (docknode, container) and their respective data structures.
* The `/graph` API endpoint now aggregates data from clients, hosts, docknodes, and containers, processing them into a React Flow-compatible format for the frontend.
* Centralized the `DockNodeHandler` instance, improving resource management and consistency.
* **Frontend UI Overhaul (`apps/dockstat`):**
* Integrated `@xyflow/react` for a powerful and interactive graph visualization experience.
* Implemented dynamic node types (`client`, `host`, `docknode`, `container`) with distinct visual representations and associated data.
* Developed a detailed `NodeDetailsPanel` to display contextual information for selected nodes, including IP, port, status, image, and IDs.
* Updated `StatsDisplay` and `Legend` components to accurately reflect the expanded infrastructure components.
* Added graph controls like `fitView`, background dots, and snap-to-grid functionality for better usability.
* **Docker Client Improvements (`packages/docker-client`):**
* Modified container mapping to include `clientId`, enabling correct association of containers with their respective client instances in the graph.
* Introduced a new `pingHost` method to check the reachability of individual hosts, improving granular status monitoring.
* Enhanced worker message handling and error reporting for greater robustness.
This commit introduces a new, interactive infrastructure graph page to visualize the entire Docker client ecosystem. The backend has been refactored to use the `dagre` library for automated and more robust node layout, replacing the previous manual positioning logic. The `/api/graph` endpoint now provides comprehensive data, including clients, hosts, docknodes, and newly added container information, leveraging enhanced data types. Key changes include: - **Backend (`apps/api`):** Migrated graph layout calculation from a custom algorithm to `dagre`. New graph calculator, helper functions, and types were introduced. The `/api/graph` response now includes detailed container information. - **Frontend (`apps/dockstat`):** Implemented a dedicated `/graph` page using React Flow. New UI components like `GraphFlow`, `NodeDetailsPanel`, `Legend`, and `StatsDisplay` were created to render the graph and its associated data dynamically. - **`docker-client` package:** Modified container information mapping to include `clientId`, ensuring containers are correctly associated with their respective clients. Host information was also enriched with `host` and `port` details for better graph representation. - **`typings` package:** Updated `DOCKER.ContainerInfo` to include `clientId` and `DockerClientManagerCore.getAllHosts` return type to include `host` and `port`. - **`ui` package:** Extended UI components with new styling variables for graph nodes and introduced a `custom` card variant for flexible node rendering. - **Navigation:** The new graph page is accessible via the sidebar navigation.
- Integrate Geist font family across the application for a modern and consistent typography. - Redesign graph visualization components (client, host, docknode, container) with an updated aesthetic. - Implement a comprehensive theming system for graph elements, utilizing new CSS variables for backgrounds, borders, and text colors (e.g., `--color-graph-client-card-bg`). - Refactor graph node components into dedicated files for better modularity and maintainability. - Update the graph `Legend` and `StatsDisplay` components to leverage the new theme variables, replacing hardcoded color classes. - Add "Container" to the graph legend and include a "Containers" count in the stats display. - Configure Vite's public directory for serving new font assets and update `bun.lock` with new dependencies.
…anagement API and frontend mutations
- **Graph Enhancements:**
- Introduced a new `dockstat` edge type to display container states (e.g., "exited", "paused") directly on edges connecting hosts to containers.
- Implemented `DockStatLabelEdge` custom edge component to render these state labels using `Card` elements, providing clear visual cues for container status.
- Disabled manual edge reconnection (`edgesReconnectable={false}`) and handle connectability (`isConnectable={false}`) in `ReactFlow` to enforce a data-driven graph structure.
- Updated container edge stroke colors to utilize CSS variables (`--color-success`, `--color-error`) for consistent theming.
- Removed `snapToGrid` from the graph flow for a smoother interaction experience.
- **API and Frontend Refactoring:**
- **API Endpoints:** Transformed Docker client and host management routes to align with RESTful conventions.
- `/client/register`, `/client/update`, `/client/delete` are now consolidated under `POST /client`, `PATCH /client`, `DELETE /client` respectively.
- `/hosts/add`, `/hosts/update`, `/hosts/delete` are now consolidated under `POST /hosts`, `PATCH /hosts`, `DELETE /hosts` respectively.
- **Frontend Mutations:** Created dedicated React hooks (`useDockerClientMutations`, `useDockerHostMutations`) to encapsulate API interaction logic, invalidation strategies, and toast notifications for Docker client and host operations.
- Migrated `AddClient` and `AddHost` components to leverage these new, centralized mutation hooks, improving code organization and reusability.
- Standardized the usage of `eden.useEdenMutation` for all plugin-related mutations, enhancing developer experience.
- **Minor Improvements:**
- Ensured consistent semicolon usage across several API files.
- Adjusted the global font weight for the "Geist" font in `index.css`.
- Refactored the `Layout` component by moving its definition to `index.tsx` for a cleaner structure.
- Added `overflow-y-scroll` to the sidebar `Card` to handle content overflow gracefully.
- Introduced dedicated mutation hooks (e.g., `useConfigMutations`, `useDockNodeMutations`, `usePluginMutations`, `useRepoMutations`, `useThemeMutations`) to consolidate data modification logic and improve maintainability. - Migrated existing components and pages to utilize these new mutation hooks, replacing direct `useEdenMutation` calls. - Standardized `react-query` data fetching by transitioning to `eden.useEdenQuery` from `@dockstat/utils/react`, leading to the deprecation and removal of the local `useEdenQuery` hook. - Applied consistent code style by removing semicolons from import and variable declarations across the codebase. - Enhanced accessibility in the dashboard by converting the `WidgetCard` interactive element from a `div` to a `button`. - Cleaned up the `/regions` API endpoint by removing an unused `body` parameter.
Introduces the core API infrastructure for managing dashboards. - The `@dockstat/widget-handler` package now includes server-side components (`src/server`) providing a `DashboardHandler` for database interactions and Elysia routes. - A new `dashboards` SQLite table is created to persist dashboard configurations, including grid layout, widget instances, and settings. - The `apps/api` integrates these new dashboard routes, exposing endpoints for CRUD (Create, Read, Update, Delete) operations on dashboards. - Elysia `t` schemas are defined within `widget-handler/src/server/model.ts` to ensure robust validation of dashboard data payloads for all API interactions. - Adds necessary dependencies (`@dockstat/logger`, `@dockstat/sqlite-wrapper`, `elysia`) to the `widget-handler` package.
…shboard support [DASH-01] - Implement backend API endpoint `/config/defaultDashboard` to persist user-selected default dashboards. - Add new Dashboard management page allowing users to create, rename, delete, and set dashboards as default. - Update the home index page to dynamically load the configured default dashboard or fall back to a placeholder. - Refactor the WidgetDrawer in the widget-handler package from a custom slide-out to a standard Modal-based palette for better UX. - Extend the database schema and TypeBox models to include default dashboard persistence in additionalSettings. - Update the sidebar navigation to include a direct link to the new Dashboards management view. - Upgrade @dockstat/sqlite-wrapper to v1.3.13 and apply project-wide formatting updates (semicolon removal).
This commit removes the entire dashboard infrastructure and the `@dockstat/widget-handler` package from the workspace. It also includes minor UX improvements for the host configuration interface. - Deleted the `@dockstat/widget-handler` package, including its server-side handlers and frontend components. - Removed dashboard-related routes and logic from the API and the main frontend application. - Replaced the dashboard rendering on the home page with a temporary placeholder. - Updated the `AddHost` component to display client names alongside IDs in the selection dropdown for better usability. - Cleaned up workspace configuration files, lockfiles, and TypeScript references to reflect the package removal.
… [N/A] Apply consistent code formatting across the monorepo. This change addresses trailing commas in TypeScript configuration files and improves the readability of React components in the dockstat application by adjusting line breaks and indentation. - Cleaned up trailing commas in apps/api/tsconfig.json and tsconfig.base.json. - Reformatted props and JSX structure in AddHost.tsx and configure.tsx for better readability. - Removed redundant whitespace and parentheses in the dockstat IndexPage.
Reviewer's GuideImplements a new infrastructure graph feature (backend graph API and layout calculation plus ReactFlow-based UI with custom node/edge components), centralizes Eden-based data mutations and queries into shared hooks, enriches Docker client/host/container types and monitoring with client IDs and per-host ping, extends configuration and database schemas (including default dashboard), and makes various UI, routing, and API refinements to support the new graph and cleaner REST conventions. ER diagram for updated configuration and host/container dataerDiagram
DockStatConfigTable ||--|{ AdditionalSettings : has
DockerHost ||--|{ DockerContainer : hosts
DockStatConfigTable {
int id
string name
json keys
json additionalSettings
json hotkeys
}
AdditionalSettings {
boolean showBackendRamUsageInNavbar
string defaultDashboard
}
DockerHost {
int id
int clientId
string name
string host
int port
boolean reachable
}
DockerContainer {
string id
int hostId
int clientId
string name
string image
string status
string state
}
Class diagram for graph types and UI node componentsclassDiagram
class GraphInput {
+clients: GraphClient[]
+hosts: GraphHost[]
+dockNodes: DockNodeArray
+containers: DOCKER_ContainerInfo[]
}
class GraphClient {
+id: number
+name: string
+initialized: boolean
}
class GraphHost {
+id: number
+name: string
+clientId: number
+reachable: boolean
+host: string
+port: number
}
class DockNodeItem {
+id: number
+name: string
+hostname: string
+port: number
+reachable: ReachableStatus
}
class ReachableStatus {
<<enumeration>>
OK
NO
DockNode_not_initialised
}
class DOCKER_ContainerInfo {
+id: string
+hostId: number
+clientId: number
+name: string
+image: string
+status: string
+state: string
}
class FlowNode {
+id: string
+type: GraphNodeType
+positionX: number
+positionY: number
+data: NodeBaseData
}
class NodeBaseData {
+label: string
+status: string
+ipAddress: string
+port: number
}
class ClientNodeData {
+label: string
+status: string
+ipAddress: string
+port: number
+clientId: number
}
class HostNodeData {
+label: string
+status: string
+ipAddress: string
+port: number
+clientId: number
+hostId: number
}
class DockNodeData {
+label: string
+status: string
+ipAddress: string
+port: number
+dockNodeId: number
}
class ContainerNodeData {
+label: string
+status: string
+image: string
+state: string
+hostId: number
+containerId: string
}
class FlowEdge {
+id: string
+edgesReconnectable: boolean
+source: string
+target: string
+animated: boolean
+styleStroke: string
+label: string
+type: string
}
class GraphNodeType {
<<enumeration>>
client
host
docknode
container
}
class GraphModel_GraphNodeSchema {
+id: string
+type: GraphNodeType
+positionX: number
+positionY: number
+data_label: string
+data_status: string
+data_ipAddress: string
+data_port: number
+data_image: string
+data_clientId: number
+data_hostId: number
}
class GraphModel_GraphEdgeSchema {
+id: string
+source: string
+target: string
+animated: boolean
+style_stroke: string
}
class ClientNodeComponent {
+render(props: Node_ClientNodeData): JSXElement
}
class HostNodeComponent {
+render(props: Node_HostNodeData): JSXElement
}
class DockNodeComponent {
+render(props: Node_DockNodeData): JSXElement
}
class ContainerNodeComponent {
+render(props: Node_ContainerNodeData): JSXElement
}
class nodeTypes_map {
+client: ClientNodeComponent
+host: HostNodeComponent
+docknode: DockNodeComponent
+container: ContainerNodeComponent
}
class calculateNodeLayout {
+calculateNodeLayout(input: GraphInput): GraphLayoutResult
}
class GraphLayoutResult {
+nodes: FlowNode[]
+edges: FlowEdge[]
}
GraphInput --> GraphClient
GraphInput --> GraphHost
GraphInput --> DockNodeItem
GraphInput --> DOCKER_ContainerInfo
DockNodeItem --> ReachableStatus
FlowNode --> NodeBaseData
ClientNodeData --|> NodeBaseData
HostNodeData --|> NodeBaseData
DockNodeData --|> NodeBaseData
FlowNode --> GraphNodeType
GraphModel_GraphNodeSchema --> GraphNodeType
GraphModel_GraphNodeSchema --> NodeBaseData
GraphModel_GraphEdgeSchema --> FlowEdge
calculateNodeLayout --> GraphLayoutResult
calculateNodeLayout --> FlowNode
calculateNodeLayout --> FlowEdge
ClientNodeComponent --> ClientNodeData
HostNodeComponent --> HostNodeData
DockNodeComponent --> DockNodeData
ContainerNodeComponent --> ContainerNodeData
nodeTypes_map --> ClientNodeComponent
nodeTypes_map --> HostNodeComponent
nodeTypes_map --> DockNodeComponent
nodeTypes_map --> ContainerNodeComponent
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 9 issues, and left some high level feedback:
- The new graph API response (
GraphModel.GraphDataSchema) doesn’t fully match whatcalculateNodeLayoutactually returns (e.g. nodedatacan containdockNodeId,containerId,state, etc., and edges can havetype: 'dockstat',label, andedgesReconnectable), which may cause runtime validation issues—consider aligning the schema with the concrete output or trimming extra fields before returning. - The updated
/frontend route now renders only<div>TEMP</div>instead of the previous backend status/health summary; if this is not intentional, consider restoring at least a minimal status view or gating the temporary placeholder behind a feature flag.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new graph API response (`GraphModel.GraphDataSchema`) doesn’t fully match what `calculateNodeLayout` actually returns (e.g. node `data` can contain `dockNodeId`, `containerId`, `state`, etc., and edges can have `type: 'dockstat'`, `label`, and `edgesReconnectable`), which may cause runtime validation issues—consider aligning the schema with the concrete output or trimming extra fields before returning.
- The updated `/` frontend route now renders only `<div>TEMP</div>` instead of the previous backend status/health summary; if this is not intentional, consider restoring at least a minimal status view or gating the temporary placeholder behind a feature flag.
## Individual Comments
### Comment 1
<location path="apps/dockstat/src/layout/hooks/useLogs.tsx" line_range="28" />
<code_context>
- </p>
+ <div>
+ <p>A server error occurred!</p>
+ <span className="text-accent">{logMessage.name}</span>]
+ </div>
),
</code_context>
<issue_to_address>
**issue (bug_risk):** There is an extra closing bracket in the error toast title.
The JSX currently ends with `</span>]`, which will render an unintended `]` in the UI. Please remove the trailing bracket so only the span content is shown.
</issue_to_address>
### Comment 2
<location path="apps/dockstat/src/components/clients/configure/AddHost.tsx" line_range="23" />
<code_context>
name: "",
hostname: "",
- clientId: registeredClients[0] || 0,
+ clientId: registeredClients[0].clientId || 0,
port: 2375,
})
</code_context>
<issue_to_address>
**issue (bug_risk):** Accessing `registeredClients[0].clientId` can throw when the list is empty.
If this component mounts with an empty `registeredClients` array, `registeredClients[0]` will be `undefined` and accessing `.clientId` will throw. Consider guarding it, e.g. `registeredClients[0]?.clientId ?? 0`, or using a safer default value.
</issue_to_address>
### Comment 3
<location path="apps/api/src/models/graph.ts" line_range="18-25" />
<code_context>
+ x: t.Number(),
+ y: t.Number(),
+ }),
+ data: t.Object({
+ label: t.String(),
+ status: t.String(),
+ ipAddress: t.Optional(t.String()),
+ port: t.Optional(t.Number()),
+ image: t.Optional(t.String()),
+ clientId: t.Optional(t.Number()),
+ hostId: t.Optional(t.Number()),
+ }),
+ })
</code_context>
<issue_to_address>
**issue (bug_risk):** Graph node schema is missing fields that `calculateNodeLayout` includes in `data`.
`GraphNodeSchema.data` only permits `label`, `status`, `ipAddress`, `port`, `image`, `clientId`, and `hostId`, but `calculateNodeLayout` also writes `dockNodeId`, `containerId`, `state`, etc. Since `t.Object` disallows additional properties by default, the handler response may fail validation or need unsafe casting. Please either update `GraphNodeSchema.data` to cover these fields or adjust `calculateNodeLayout` to emit only schema-defined properties.
</issue_to_address>
### Comment 4
<location path="apps/api/src/models/graph.ts" line_range="29-34" />
<code_context>
+ }),
+ })
+
+ export const GraphEdgeSchema = t.Object({
+ id: t.String(),
+ source: t.String(),
+ target: t.String(),
+ animated: t.Optional(t.Boolean()),
+ style: t.Optional(
+ t.Object({
+ stroke: t.Optional(t.String()),
</code_context>
<issue_to_address>
**issue (bug_risk):** Graph edge schema does not match the edge shape produced by `calculateNodeLayout`.
`FlowEdge` and `calculateNodeLayout` include fields like `edgesReconnectable`, `label`, and container `type: "dockstat"`, but `GraphEdgeSchema` only validates `id`, `source`, `target`, `animated`, and `style.stroke`. This discrepancy can cause validation failures at runtime. Either extend `GraphEdgeSchema` to cover these fields or ensure `calculateNodeLayout` removes them before returning the edges.
</issue_to_address>
### Comment 5
<location path="apps/dockstat/src/components/graph/flow.tsx" line_range="66" />
<code_context>
+ )
+ }
+
+ const hasNoData = !data || (data && "nodes" in data && data.nodes?.length === 0)
+
+ return (
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The `hasNoData` check will treat error responses without `nodes` as "no data" instead of an error.
When the backend returns the 400 shape `{ success: false, error }`, the eden wrapper may still expose that as `data`. In that case, `"nodes" in data` is false, so `hasNoData` is false and neither the error nor empty state renders. Consider explicitly handling the success/error shape when computing `hasNoData` (e.g. `if ('success' in data && data.success === false) ...`), consistent with how you decide whether to call `setNodes`/`setEdges`.
```suggestion
// Treat explicit backend errors (`{ success: false, error }`) as errors, not "no data".
// Only consider it "no data" when:
// - there is no `data`, or
// - the request was successful (`success` absent or true) and there are zero nodes.
const hasNoData =
!data ||
(("success" in data ? data.success : true) &&
"nodes" in data &&
Array.isArray((data as any).nodes) &&
data.nodes.length === 0)
```
</issue_to_address>
### Comment 6
<location path="packages/ui/src/components/Forms/Selection.tsx" line_range="18" />
<code_context>
disabled?: boolean
placeholder?: string
value?: string
+ rawOnChange?: (e: React.ChangeEvent<HTMLSelectElement, HTMLSelectElement>) => void
onChange?: (value: string) => void
className?: string
</code_context>
<issue_to_address>
**issue (bug_risk):** The `rawOnChange` type uses an incorrect `React.ChangeEvent` generic signature.
`React.ChangeEvent` only takes a single generic for the target element, so this should be `React.ChangeEvent<HTMLSelectElement>`. The two-parameter form is invalid and may either produce a type error or be ignored by TypeScript.
</issue_to_address>
### Comment 7
<location path="apps/dockstat/src/layout/index.tsx" line_range="71-63" />
<code_context>
-
- return pinMutation.mutateAsync(input)
- },
- unpin: (input: { path: string; slug: string }) => {
- toast({
- title: `Unpinned "${input.slug}"!`,
- description: (
- <span>
- Added a new pinned link: "{input.slug}" - <pre>{input.path}</pre>
- </span>
- ),
</code_context>
<issue_to_address>
**nitpick (bug_risk):** Unpin toast message still says "Added a new pinned link".
The `unpin` toast currently reuses the `pin` description (`Added a new pinned link`). Please update the description to clearly state that a link was unpinned/removed instead.
</issue_to_address>
### Comment 8
<location path="apps/dockstat/src/layout/index.tsx" line_range="10" />
<code_context>
-import { toast } from "@/lib/toast"
-import { useLayout } from "../hooks/useLayout"
-
-export function Layout({ children }: { children: React.ReactNode }) {
- const {
- ramUsage,
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the Layout module by extracting helper functions for pin/unpin handlers and precomputed props, and by aliasing the useLayout hook to avoid duplicate import/export in the same file.
You can trim the complexity of `Layout` without changing behavior by pulling the small bits of logic out of the JSX and avoiding the duplicate `useLayout` import/export.
### 1. Extract pin/unpin logic out of JSX
Right now, the `mutationFn` object embeds toast + mutation logic inline. Moving it into a helper keeps the JSX declarative and makes behavior easier to test.
```ts
type PinInput = { path: string; slug: string }
function createPinMutationHandlers({
pinMutation,
unPinMutation,
isBusy,
}: {
pinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
unPinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
isBusy: boolean
}) {
const notify = (action: "Pinned" | "Unpinned", input: PinInput) =>
toast({
title: `${action} "${input.slug}"!`,
description: (
<span>
Added a new pinned link: "{input.slug}" - <pre>{input.path}</pre>
</span>
),
variant: "success",
})
return {
pin: (input: PinInput) => {
notify("Pinned", input)
return pinMutation.mutateAsync(input)
},
unpin: (input: PinInput) => {
notify("Unpinned", input)
return unPinMutation.mutateAsync(input)
},
isBusy,
}
}
```
Then in `Layout`:
```tsx
const mutationFn = createPinMutationHandlers({ pinMutation, unPinMutation, isBusy })
return (
<div className="bg-main-bg min-h-screen w-screen p-4">
<Toaster expand position="bottom-right" />
<Navbar
// ...
mutationFn={mutationFn}
// ...
/>
{/* ... */}
</div>
)
```
This keeps the JSX mostly “data-shaped” props instead of embedded behavior.
### 2. Precompute shaped props above JSX
Similarly, move the small mapping/selection logic into local constants right before the `return`:
```tsx
const sidebarHotkeys = {
close: config.hotkeys?.["close:sidebar"],
open: config.hotkeys?.["open:sidebar"],
toggle: config.hotkeys?.["toggle:sidebar"],
}
const ramUsageForNavbar = config.additionalSettings?.showBackendRamUsageInNavbar
? ramUsage
: undefined
const pluginLinks = frontendPluginRoutes || []
const navLinks = config?.navLinks || []
```
and use them in JSX:
```tsx
<Navbar
sidebarHotkeys={sidebarHotkeys}
ramUsage={ramUsageForNavbar}
pluginLinks={pluginLinks}
navLinks={navLinks}
// ...
/>
```
This shortens the JSX, making it easier to scan.
### 3. Avoid duplicate `useLayout` import/re-export in the same file
You can keep the re-export while using a single import locally by aliasing:
```ts
import { useLayout as useLayoutHook } from "./hooks/useLayout"
export { useLayoutHook as useLayout }
export function Layout({ children }: { children: React.ReactNode }) {
const {
ramUsage,
// ...
} = useLayoutHook()
// ...
}
```
This removes the double import/re-export smell and clarifies that this file is both a container and a re-export, without changing behavior.
</issue_to_address>
### Comment 9
<location path="apps/api/src/graph/graphCalculator.ts" line_range="17" />
<code_context>
+
+const logger = BaseLogger.spawn("GraphCalc")
+
+export function calculateNodeLayout(input: GraphInput): {
+ nodes: FlowNode[]
+ edges: FlowEdge[]
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring `calculateNodeLayout` into smaller helpers and shared utilities to separate orchestration from per-entity details and logging noise.
You can keep the behavior intact while reducing complexity by extracting a few focused helpers and centralizing the repetitive patterns.
### 1. Split `calculateNodeLayout` into orchestration + helpers
Move node/edge registration and flow graph construction into separate pure helpers. This keeps `calculateNodeLayout` readable at a glance:
```ts
export function calculateNodeLayout(input: GraphInput): { nodes: FlowNode[]; edges: FlowEdge[] } {
const { clients, hosts, dockNodes, containers } = input;
logger.info("Starting graph layout calculation...");
logger.debug(
`Input counts: Clients=${clients.length}, Hosts=${hosts.length}, DockNodes=${dockNodes.length}, Containers=${containers.length}`
);
const g = createDagreGraph();
registerNodes(g, input);
registerEdgesForLayout(g, input);
logger.info("Running Dagre layout algorithm...");
dagre.layout(g);
logger.info("Dagre layout complete.");
const { nodes, edges } = buildFlowGraphFromDagre(g, input);
logger.info(`Graph layout finished. Generated ${nodes.length} nodes and ${edges.length} edges.`);
return { nodes, edges };
}
function createDagreGraph(): dagre.graphlib.Graph {
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: "LR", nodesep: 20, ranksep: 50, marginx: 50, marginy: 50 });
g.setDefaultEdgeLabel(() => ({}));
return g;
}
function registerNodes(g: dagre.graphlib.Graph, input: GraphInput): void {
const { clients, hosts, dockNodes, containers } = input;
logger.info("Registering nodes...");
const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
logger.debug(`Registering node in Dagre: [${type}] ${id}`);
g.setNode(id, { width: 250, height: 80 });
};
for (const c of clients) addNodeToGraph(getClientId(c.id), "Client");
for (const h of hosts) addNodeToGraph(getHostId(h.clientId, h.id), "Host");
for (const co of containers) addNodeToGraph(getContainerId(co.id), "Container");
for (const d of dockNodes) addNodeToGraph(getDockNodeId(d.id), "DockNode");
}
function registerEdgesForLayout(g: dagre.graphlib.Graph, input: GraphInput): void {
const { hosts, containers } = input;
logger.info("Registering edges for layout calculation...");
hosts.forEach((host) => {
const source = getClientId(host.clientId);
const target = getHostId(host.clientId, host.id);
g.setEdge(source, target);
});
containers.forEach((container) => {
const source = getHostId(container.clientId, container.hostId);
const target = getContainerId(container.id);
if (g.hasNode(source)) {
g.setEdge(source, target);
} else {
logger.warn(
`Container ${container.id} references non-existent Host ${source}. Edge skipped.`
);
}
});
}
```
### 2. Factor out repeated Dagre-node → FlowNode mapping
The logic for clients/hosts/containers/dockNodes shares a pattern: resolve Dagre node, guard, build `FlowNode`. You can use a small helper that centralizes the guard and position calculation:
```ts
function buildFlowNode(
g: dagre.graphlib.Graph,
nodeId: string,
type: FlowNode["type"],
data: FlowNode["data"]
): FlowNode | null {
const dagreNode = g.node(nodeId);
if (!dagreNode) {
logger.error(`Missing node in Dagre output: ${nodeId}`);
return null;
}
return {
id: nodeId,
type,
position: calculatePosition(dagreNode),
data,
};
}
```
Then `buildFlowGraphFromDagre` can stay concise:
```ts
function buildFlowGraphFromDagre(
g: dagre.graphlib.Graph,
input: GraphInput
): { nodes: FlowNode[]; edges: FlowEdge[] } {
const { clients, hosts, containers, dockNodes } = input;
const nodes: FlowNode[] = [];
const edges: FlowEdge[] = [];
clients.forEach((client) => {
const flowNode = buildFlowNode(g, getClientId(client.id), "client", {
label: client.name || `Client ${client.id}`,
status: client.initialized ? "online" : "offline",
clientId: client.id,
});
if (flowNode) nodes.push(flowNode);
});
hosts.forEach((host) => {
const hostId = getHostId(host.clientId, host.id);
const flowNode = buildFlowNode(g, hostId, "host", {
label: host.name || `Host ${host.id}`,
status: host.reachable ? "online" : "offline",
ipAddress: host.host,
port: host.port,
clientId: host.clientId,
hostId: host.id,
});
if (flowNode) nodes.push(flowNode);
edges.push(
createEdge(
getClientId(host.clientId),
hostId,
host.reachable,
getEdgeStrokeColor(host.reachable)
)
);
});
// containers & dockNodes follow same pattern, using buildFlowNode
// ...
return { nodes, edges };
}
```
This removes the repeated `g.node(...)` / null-guard / `calculatePosition(...)` boilerplate.
### 3. Gate verbose logging behind a flag
The per-node debug logs are useful but noisy. If `BaseLogger` doesn’t already support it, a simple flag can keep the main algorithm readable while preserving logs:
```ts
const logger = BaseLogger.spawn("GraphCalc");
const DEBUG_GRAPH_LAYOUT = process.env.DEBUG_GRAPH_LAYOUT === "true";
function debugLog(message: string) {
if (DEBUG_GRAPH_LAYOUT) {
logger.debug(message);
}
}
// usage in registerNodes:
const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
debugLog(`Registering node in Dagre: [${type}] ${id}`);
g.setNode(id, { width: 250, height: 80 });
};
```
This keeps the key `info`/`warn`/`error` logs but hides the verbose details unless explicitly enabled.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| import { toast } from "@/lib/toast" | ||
| import { useLayout } from "./hooks/useLayout" | ||
|
|
||
| export function Layout({ children }: { children: React.ReactNode }) { |
There was a problem hiding this comment.
issue (complexity): Consider simplifying the Layout module by extracting helper functions for pin/unpin handlers and precomputed props, and by aliasing the useLayout hook to avoid duplicate import/export in the same file.
You can trim the complexity of Layout without changing behavior by pulling the small bits of logic out of the JSX and avoiding the duplicate useLayout import/export.
1. Extract pin/unpin logic out of JSX
Right now, the mutationFn object embeds toast + mutation logic inline. Moving it into a helper keeps the JSX declarative and makes behavior easier to test.
type PinInput = { path: string; slug: string }
function createPinMutationHandlers({
pinMutation,
unPinMutation,
isBusy,
}: {
pinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
unPinMutation: { mutateAsync: (input: PinInput) => Promise<unknown> }
isBusy: boolean
}) {
const notify = (action: "Pinned" | "Unpinned", input: PinInput) =>
toast({
title: `${action} "${input.slug}"!`,
description: (
<span>
Added a new pinned link: "{input.slug}" - <pre>{input.path}</pre>
</span>
),
variant: "success",
})
return {
pin: (input: PinInput) => {
notify("Pinned", input)
return pinMutation.mutateAsync(input)
},
unpin: (input: PinInput) => {
notify("Unpinned", input)
return unPinMutation.mutateAsync(input)
},
isBusy,
}
}Then in Layout:
const mutationFn = createPinMutationHandlers({ pinMutation, unPinMutation, isBusy })
return (
<div className="bg-main-bg min-h-screen w-screen p-4">
<Toaster expand position="bottom-right" />
<Navbar
// ...
mutationFn={mutationFn}
// ...
/>
{/* ... */}
</div>
)This keeps the JSX mostly “data-shaped” props instead of embedded behavior.
2. Precompute shaped props above JSX
Similarly, move the small mapping/selection logic into local constants right before the return:
const sidebarHotkeys = {
close: config.hotkeys?.["close:sidebar"],
open: config.hotkeys?.["open:sidebar"],
toggle: config.hotkeys?.["toggle:sidebar"],
}
const ramUsageForNavbar = config.additionalSettings?.showBackendRamUsageInNavbar
? ramUsage
: undefined
const pluginLinks = frontendPluginRoutes || []
const navLinks = config?.navLinks || []and use them in JSX:
<Navbar
sidebarHotkeys={sidebarHotkeys}
ramUsage={ramUsageForNavbar}
pluginLinks={pluginLinks}
navLinks={navLinks}
// ...
/>This shortens the JSX, making it easier to scan.
3. Avoid duplicate useLayout import/re-export in the same file
You can keep the re-export while using a single import locally by aliasing:
import { useLayout as useLayoutHook } from "./hooks/useLayout"
export { useLayoutHook as useLayout }
export function Layout({ children }: { children: React.ReactNode }) {
const {
ramUsage,
// ...
} = useLayoutHook()
// ...
}This removes the double import/re-export smell and clarifies that this file is both a container and a re-export, without changing behavior.
|
|
||
| const logger = BaseLogger.spawn("GraphCalc") | ||
|
|
||
| export function calculateNodeLayout(input: GraphInput): { |
There was a problem hiding this comment.
issue (complexity): Consider refactoring calculateNodeLayout into smaller helpers and shared utilities to separate orchestration from per-entity details and logging noise.
You can keep the behavior intact while reducing complexity by extracting a few focused helpers and centralizing the repetitive patterns.
1. Split calculateNodeLayout into orchestration + helpers
Move node/edge registration and flow graph construction into separate pure helpers. This keeps calculateNodeLayout readable at a glance:
export function calculateNodeLayout(input: GraphInput): { nodes: FlowNode[]; edges: FlowEdge[] } {
const { clients, hosts, dockNodes, containers } = input;
logger.info("Starting graph layout calculation...");
logger.debug(
`Input counts: Clients=${clients.length}, Hosts=${hosts.length}, DockNodes=${dockNodes.length}, Containers=${containers.length}`
);
const g = createDagreGraph();
registerNodes(g, input);
registerEdgesForLayout(g, input);
logger.info("Running Dagre layout algorithm...");
dagre.layout(g);
logger.info("Dagre layout complete.");
const { nodes, edges } = buildFlowGraphFromDagre(g, input);
logger.info(`Graph layout finished. Generated ${nodes.length} nodes and ${edges.length} edges.`);
return { nodes, edges };
}
function createDagreGraph(): dagre.graphlib.Graph {
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: "LR", nodesep: 20, ranksep: 50, marginx: 50, marginy: 50 });
g.setDefaultEdgeLabel(() => ({}));
return g;
}
function registerNodes(g: dagre.graphlib.Graph, input: GraphInput): void {
const { clients, hosts, dockNodes, containers } = input;
logger.info("Registering nodes...");
const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
logger.debug(`Registering node in Dagre: [${type}] ${id}`);
g.setNode(id, { width: 250, height: 80 });
};
for (const c of clients) addNodeToGraph(getClientId(c.id), "Client");
for (const h of hosts) addNodeToGraph(getHostId(h.clientId, h.id), "Host");
for (const co of containers) addNodeToGraph(getContainerId(co.id), "Container");
for (const d of dockNodes) addNodeToGraph(getDockNodeId(d.id), "DockNode");
}
function registerEdgesForLayout(g: dagre.graphlib.Graph, input: GraphInput): void {
const { hosts, containers } = input;
logger.info("Registering edges for layout calculation...");
hosts.forEach((host) => {
const source = getClientId(host.clientId);
const target = getHostId(host.clientId, host.id);
g.setEdge(source, target);
});
containers.forEach((container) => {
const source = getHostId(container.clientId, container.hostId);
const target = getContainerId(container.id);
if (g.hasNode(source)) {
g.setEdge(source, target);
} else {
logger.warn(
`Container ${container.id} references non-existent Host ${source}. Edge skipped.`
);
}
});
}2. Factor out repeated Dagre-node → FlowNode mapping
The logic for clients/hosts/containers/dockNodes shares a pattern: resolve Dagre node, guard, build FlowNode. You can use a small helper that centralizes the guard and position calculation:
function buildFlowNode(
g: dagre.graphlib.Graph,
nodeId: string,
type: FlowNode["type"],
data: FlowNode["data"]
): FlowNode | null {
const dagreNode = g.node(nodeId);
if (!dagreNode) {
logger.error(`Missing node in Dagre output: ${nodeId}`);
return null;
}
return {
id: nodeId,
type,
position: calculatePosition(dagreNode),
data,
};
}Then buildFlowGraphFromDagre can stay concise:
function buildFlowGraphFromDagre(
g: dagre.graphlib.Graph,
input: GraphInput
): { nodes: FlowNode[]; edges: FlowEdge[] } {
const { clients, hosts, containers, dockNodes } = input;
const nodes: FlowNode[] = [];
const edges: FlowEdge[] = [];
clients.forEach((client) => {
const flowNode = buildFlowNode(g, getClientId(client.id), "client", {
label: client.name || `Client ${client.id}`,
status: client.initialized ? "online" : "offline",
clientId: client.id,
});
if (flowNode) nodes.push(flowNode);
});
hosts.forEach((host) => {
const hostId = getHostId(host.clientId, host.id);
const flowNode = buildFlowNode(g, hostId, "host", {
label: host.name || `Host ${host.id}`,
status: host.reachable ? "online" : "offline",
ipAddress: host.host,
port: host.port,
clientId: host.clientId,
hostId: host.id,
});
if (flowNode) nodes.push(flowNode);
edges.push(
createEdge(
getClientId(host.clientId),
hostId,
host.reachable,
getEdgeStrokeColor(host.reachable)
)
);
});
// containers & dockNodes follow same pattern, using buildFlowNode
// ...
return { nodes, edges };
}This removes the repeated g.node(...) / null-guard / calculatePosition(...) boilerplate.
3. Gate verbose logging behind a flag
The per-node debug logs are useful but noisy. If BaseLogger doesn’t already support it, a simple flag can keep the main algorithm readable while preserving logs:
const logger = BaseLogger.spawn("GraphCalc");
const DEBUG_GRAPH_LAYOUT = process.env.DEBUG_GRAPH_LAYOUT === "true";
function debugLog(message: string) {
if (DEBUG_GRAPH_LAYOUT) {
logger.debug(message);
}
}
// usage in registerNodes:
const addNodeToGraph = (id: string, type: "Client" | "Host" | "Container" | "DockNode") => {
debugLog(`Registering node in Dagre: [${type}] ${id}`);
g.setNode(id, { width: 250, height: 80 });
};This keeps the key info/warn/error logs but hides the verbose details unless explicitly enabled.
There was a problem hiding this comment.
Pull request overview
Adds an “Infrastructure Graph” feature spanning API → data/layout generation → frontend ReactFlow rendering, alongside a broader frontend hook refactor (moving Eden query/mutation usage to @dockstat/utils/react and app-local mutation wrappers) and some related API route cleanups.
Changes:
- Introduce
/api/v2/graphendpoint that assembles clients/hosts/containers/docknodes and generates positioned ReactFlow nodes/edges via Dagre. - Add ReactFlow-based graph page/UI (new
/graphroute) plus UI-library node components + theme variables for graph styling. - Refactor many frontend API calls to use
eden.*hooks from@dockstat/utils/reactand new mutation wrapper hooks; adjust Docker host/client routes to RESTful verbs/paths.
Reviewed changes
Copilot reviewed 100 out of 104 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ui/src/components/index.ts | Re-export graph node components/types from UI package. |
| packages/ui/src/components/Sidebar/Sidebar.tsx | Sidebar prop formatting + enable vertical scrolling on Card. |
| packages/ui/src/components/Navbar/consts.ts | Add /dashboards and /graph items to sidebar paths. |
| packages/ui/src/components/Navbar/Navbar.tsx | Adjust navbar layout + NavLink active matching (end). |
| packages/ui/src/components/Modal/Modal.tsx | Allow modal title to be a ReactNode. |
| packages/ui/src/components/Link/Link.tsx | Add end to NavLink for exact matching. |
| packages/ui/src/components/Graph/nodes/host.tsx | New ReactFlow Host node UI. |
| packages/ui/src/components/Graph/nodes/docknode.tsx | New ReactFlow DockNode node UI. |
| packages/ui/src/components/Graph/nodes/container.tsx | New ReactFlow Container node UI. |
| packages/ui/src/components/Graph/nodes/client.tsx | New ReactFlow Client node UI. |
| packages/ui/src/components/Graph/index.tsx | Export node components + nodeTypes mapping for ReactFlow. |
| packages/ui/src/components/Forms/Selection.tsx | Add rawOnChange option to Select component. |
| packages/ui/src/components/Forms/Input.tsx | Convert Input component from React.FC to function + remove React import. |
| packages/ui/src/components/Card/Card.tsx | Add custom card variant option. |
| packages/ui/src/App.css | Add CSS variables for graph node styling. |
| packages/ui/package.json | Add ReactFlow + cn dependency for UI package. |
| packages/typings/src/typebox/db.ts | Add defaultDashboard to config schema. |
| packages/typings/src/docker-client.ts | Add clientId to ContainerInfo. |
| packages/sqlite-wrapper/src/index.ts | Type tightening + formatting improvements + better error throw formatting. |
| packages/sqlite-wrapper/package.json | Bump sqlite-wrapper version. |
| packages/docker-client/src/shared/types.ts | Add pingHost worker request type. |
| packages/docker-client/src/manager/utils/sendWorkerMessage.ts | Resolve timeouts/crashes into WorkerResponse instead of rejecting; add error listener. |
| packages/docker-client/src/manager/hosts.ts | Include host/port in mapped host output. |
| packages/docker-client/src/manager/core.ts | Adjust sendWorkerMessage typing + ping behavior + host type expansion. |
| packages/docker-client/src/client/utils/mapContainerInfo.ts | Include clientId in mapped container info. |
| packages/docker-client/src/client/mixins/monitoring/index.ts | Pass clientId into monitoring manager. |
| packages/docker-client/src/client/mixins/hosts/index.ts | Add pingHost method; reformat thrown Errors. |
| packages/docker-client/src/client/mixins/core/base.ts | Pass clientId into monitoring manager. |
| packages/docker-client/src/client/mixins/containers/index.ts | Thread clientId through container mapping + formatting. |
| packages/docker-client/src/client/managers/monitoring/monitors/ContainerMetrics.ts | Track clientId; include in mapped container info; formatting. |
| packages/docker-client/src/client/managers/monitoring/monitors/ContainerEvents.ts | Track clientId; include in mapped container info. |
| packages/docker-client/src/client/managers/monitoring/index.ts | Plumb clientId into monitoring subcomponents. |
| packages/docker-client/src/client/managers/monitoring/eventStreamMonitor.ts | Plumb clientId into container inspect mapping. |
| packages/docker-client/src/client/managers/host-handler/index.ts | Use select([\"*\"]) instead of all() for hosts. |
| packages/docker-client/src/_worker.index.ts | Add pingHost handling + formatting. |
| packages/db/defaults.ts | Add defaultDashboard default config entry. |
| package.json | Add new dev script, bump Turbo, add React DOM to catalog, bump type deps. |
| apps/dockstat/vite.config.ts | Set publicDir for Vite build. |
| apps/dockstat/src/router.tsx | Register /graph route. |
| apps/dockstat/src/providers/themeSidebar.tsx | Switch to centralized theme mutations hook. |
| apps/dockstat/src/providers/theme.tsx | Switch queries to eden.useEdenQuery; wrap theme create mutation behavior. |
| apps/dockstat/src/providers/additionalSettings.tsx | Switch to eden.useEdenQuery. |
| apps/dockstat/src/pages/node/index.tsx | Switch to eden.useEdenQuery + docknode mutation wrappers. |
| apps/dockstat/src/pages/index.tsx | Replace Home content with placeholder. |
| apps/dockstat/src/pages/graph/index.tsx | New graph page hosting ReactFlow provider + GraphFlow. |
| apps/dockstat/src/pages/extensions/plugins.tsx | Switch to eden.useEdenQuery + plugin mutation wrappers. |
| apps/dockstat/src/pages/extensions/index.tsx | Switch to eden.useEdenQuery + repo mutation wrapper. |
| apps/dockstat/src/pages/clients/index.tsx | Switch to eden.useEdenQuery. |
| apps/dockstat/src/pages/clients/configure.tsx | Switch to eden.useEdenQuery; adjust AddHost registered client shape. |
| apps/dockstat/src/layout/index.tsx | Inline Layout implementation into layout index barrel. |
| apps/dockstat/src/layout/hooks/useThemeManager.ts | Cleanup effect deps; add useRef import. |
| apps/dockstat/src/layout/hooks/usePluginRoutes.ts | Switch to eden.useEdenQuery. |
| apps/dockstat/src/layout/hooks/usePinMutations.ts | Switch to centralized config mutations hook. |
| apps/dockstat/src/layout/hooks/useLogs.tsx | Adjust toast title rendering; log errors to console. |
| apps/dockstat/src/layout/hooks/useDeleteTheme.ts | Switch to centralized theme mutations hook. |
| apps/dockstat/src/layout/components/Layout.tsx | Remove old Layout component (moved to layout index). |
| apps/dockstat/src/index.css | Add ReactFlow attribution styling + add Geist font faces + theme font vars. |
| apps/dockstat/src/hooks/useEdenQuery.ts | Remove old local Eden query helper (migrated to utils). |
| apps/dockstat/src/hooks/plugins/usePluginTemplate.ts | Switch to plugin template mutation wrapper. |
| apps/dockstat/src/hooks/plugins/usePluginLoaders.ts | Switch to eden.useEdenRouteMutation. |
| apps/dockstat/src/hooks/plugins/usePluginActions.ts | Switch to eden.useEdenRouteMutation. |
| apps/dockstat/src/hooks/mutations/theme.ts | New theme mutation wrappers. |
| apps/dockstat/src/hooks/mutations/repo.ts | New repo mutation wrappers. |
| apps/dockstat/src/hooks/mutations/plugin.ts | New plugin mutation wrappers. |
| apps/dockstat/src/hooks/mutations/node.ts | New DockNode mutation wrappers. |
| apps/dockstat/src/hooks/mutations/index.ts | Barrel export for mutation wrappers. |
| apps/dockstat/src/hooks/mutations/docker-host.ts | New docker-host mutation wrappers. |
| apps/dockstat/src/hooks/mutations/docker-client.ts | New docker-client mutation wrappers. |
| apps/dockstat/src/hooks/mutations/dashboard.ts | New dashboard mutation wrappers (incl. default dashboard). |
| apps/dockstat/src/hooks/mutations/config.ts | New config mutation wrappers (pin/unpin/settings/hotkeys). |
| apps/dockstat/src/hooks/eden/useEdenMutation.ts | Add edenMutation alias export. |
| apps/dockstat/src/contexts/theme.ts | Re-type theme mutation result using eden.MutationResult. |
| apps/dockstat/src/components/settings/hotkeys.tsx | Switch to centralized config mutations hook. |
| apps/dockstat/src/components/settings/general/sections/useGeneralSettings.tsx | Switch to eden.useEdenQuery + centralized config mutations. |
| apps/dockstat/src/components/graph/statsDisplay.tsx | New graph stats pill display component. |
| apps/dockstat/src/components/graph/nodeDetails.tsx | New selected-node details panel component. |
| apps/dockstat/src/components/graph/legend.tsx | New legend sidebar component. |
| apps/dockstat/src/components/graph/flow.tsx | New ReactFlow graph canvas + sidebar + data fetching. |
| apps/dockstat/src/components/graph/customEdge.tsx | New labeled edge renderer using EdgeLabelRenderer. |
| apps/dockstat/src/components/extensions/RepoCard.tsx | Switch to repo delete mutation wrapper. |
| apps/dockstat/src/components/clients/configure/AddHost.tsx | Switch to docker-host mutation wrappers + richer client selection UI. |
| apps/dockstat/src/components/clients/configure/AddClient.tsx | Switch to docker-client mutation wrapper. |
| apps/dockstat/public/fonts/GeistPixel-Square.woff2 | Add Geist Pixel font asset. |
| apps/dockstat/package.json | Add ReactFlow + Geist deps. |
| apps/api/src/routes/node/index.ts | Use shared DockNodeHandler instance; formatting. |
| apps/api/src/routes/graph/index.ts | New /graph API route returning nodes/edges + entity lists. |
| apps/api/src/routes/docker/hosts.ts | Convert host routes to RESTful / + patch/delete verbs. |
| apps/api/src/routes/docker/client.ts | Convert client routes to RESTful / + patch/delete verbs. |
| apps/api/src/routes/db.ts | Add config endpoint to set/clear defaultDashboard. |
| apps/api/src/models/graph.ts | New response schemas for graph endpoint. |
| apps/api/src/models/database.ts | Expand additional settings response schema; formatting. |
| apps/api/src/middleware/metrics/helper.ts | Make header access optional-safe. |
| apps/api/src/index.ts | Register Graph routes. |
| apps/api/src/handlers/requestLogger.ts | Make header access optional-safe. |
| apps/api/src/graph/types.ts | New types for graph layout input/output. |
| apps/api/src/graph/reachableStatus.ts | Map docknode reachability to OK/NO/not-initialised. |
| apps/api/src/graph/index.ts | Export graph calculator + types. |
| apps/api/src/graph/helper.ts | Shared ID/styling helpers for graph generation. |
| apps/api/src/graph/graphCalculator.ts | Dagre-based graph layout + ReactFlow node/edge generation. |
| apps/api/src/docker/docknode.ts | Centralize DockNodeHandler instance (DNH). |
| apps/api/package.json | Add dagre + typings; reorder deps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This commit applies several updates across the API and frontend to improve data consistency and user interface feedback: - Updated GraphModel schema in the API to include new optional fields: dockNodeId, state, containerId, label, type, and edgesReconnectable. - Improved null safety in AddHost component when accessing registered client IDs. - Enhanced data validation logic in GraphFlow to correctly handle success flags and empty node arrays. - Fixed a typo in the log toast notification and corrected the success message when unpinning items in the layout. - Cleaned up TypeScript event handler definitions in the Selection UI component. - Removed a redundant ping request within the DockerClientManagerCore loop to optimize performance.
…in GraphFlow [N/A] Removed a redundant 'as any' type assertion when checking the nodes array in the GraphFlow component. Additionally, corrected the indentation of the hasNoData constant to align with project coding standards.
Summary by Sourcery
Add an infrastructure graph feature visualising clients, hosts, DockNodes and containers, refactor frontend API/mutation hooks to use shared Eden-based utilities, and extend backend and typings to support graph data, host details, dashboards, and configuration updates.
New Features:
Bug Fixes:
Enhancements: