feat(resources): add network management with create, inspect, and topology visualization (#335)

* feat(resources): add network management with create, inspect, and topology visualization

Add full Docker network CRUD: create networks with custom drivers, subnets,
and IPAM config; inspect network details including connected containers and
IP addresses; interactive topology graph visualization (Pro-gated to
Skipper/Admiral tiers). Includes backend routes, DockerController methods,
unit tests, documentation with screenshots, and updated changelog.

* refactor(resources): address code review findings for network management

- Add batch GET /api/system/networks/topology endpoint to eliminate N+1
  HTTP calls from the topology view
- Export DockerNetwork type from ResourcesView, remove duplicate in
  NetworkTopologyView
- Wire up isInspectLoading state to Eye button (spinner + disabled)
- Remove unnecessary wrapper div around FilterToggle
- NetworkTopologyView is now self-contained (fetches from batch endpoint,
  no longer needs networks prop)

* fix(resources): align pre-existing UI with design system standards

- Replace hardcoded red-500 on image/volume delete buttons with
  text-destructive/60 hover:bg-destructive tokens
- Replace shadow-sm with shadow-card-bevel on all cards (Disk Footprint,
  Quick Clean, Resource Tabs, Unmanaged Containers)
- Add strokeWidth={1.5} to all action button Trash2 icons
- Type network drivers as union type instead of raw strings (backend
  NetworkDriver type + frontend NETWORK_DRIVERS constant)

* fix(resources): add generic type args to useNodesState/useEdgesState

Fixes TS2345 build error in CI where tsc -b (strict mode via
tsconfig.app.json) infers never[] from untyped empty array literals
passed to React Flow hooks.

* fix(resources): replace explicit any in catch blocks with unknown narrowing

ESLint no-explicit-any errors in CI for three catch blocks added by the
network management feature.
This commit is contained in:
Anso
2026-04-02 14:53:11 -04:00
committed by GitHub
parent c35ee2ed05
commit 4488637656
13 changed files with 1041 additions and 33 deletions
+70 -1
View File
@@ -5,7 +5,7 @@ import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import WebSocket, { WebSocketServer } from 'ws';
import jwt from 'jsonwebtoken';
import DockerController, { globalDockerNetwork } from './services/DockerController';
import DockerController, { globalDockerNetwork, type CreateNetworkOptions } from './services/DockerController';
import { FileSystemService } from './services/FileSystemService';
import { ComposeService } from './services/ComposeService';
import bcrypt from 'bcrypt';
@@ -4405,6 +4405,75 @@ app.post('/api/system/networks/delete', async (req: Request, res: Response) => {
}
});
app.get('/api/system/networks/topology', async (req: Request, res: Response) => {
try {
const dockerController = DockerController.getInstance(req.nodeId);
const allNetworks = await dockerController.getNetworks();
const userNetworks = allNetworks.filter((n: any) => n.managedStatus !== 'system');
const inspected = await Promise.all(
userNetworks.map(async (net: any) => {
try {
const detail = await dockerController.inspectNetwork(net.Id);
const containers = Object.entries(detail.Containers || {}).map(([id, c]: [string, any]) => ({
id,
name: c.Name,
ip: c.IPv4Address,
}));
return { ...net, containers };
} catch {
return { ...net, containers: [] };
}
})
);
res.json(inspected);
} catch (error: any) {
console.error('Failed to fetch network topology:', error);
res.status(500).json({ error: error.message || 'Failed to fetch network topology' });
}
});
app.get('/api/system/networks/:id', async (req: Request, res: Response) => {
try {
const id = req.params.id as string;
if (!id) return res.status(400).json({ error: 'Network ID is required' });
const dockerController = DockerController.getInstance(req.nodeId);
const networkInfo = await dockerController.inspectNetwork(id);
res.json(networkInfo);
} catch (error: any) {
console.error('Failed to inspect network:', error);
res.status(500).json({ error: error.message || 'Failed to inspect network' });
}
});
app.post('/api/system/networks', async (req: Request, res: Response) => {
if (!requireAdmin(req, res)) return;
try {
const { name, driver, subnet, gateway, labels, internal, attachable } = req.body;
if (!name) return res.status(400).json({ error: 'Network name is required' });
const options: CreateNetworkOptions = { Name: name };
if (driver) options.Driver = driver;
if (subnet || gateway) {
options.IPAM = { Config: [{}] };
if (subnet) options.IPAM.Config[0].Subnet = subnet;
if (gateway) options.IPAM.Config[0].Gateway = gateway;
}
if (labels && typeof labels === 'object') options.Labels = labels;
if (internal) options.Internal = true;
if (attachable) options.Attachable = true;
const dockerController = DockerController.getInstance(req.nodeId);
const network = await dockerController.createNetwork(options);
res.status(201).json({ success: true, message: 'Network created', id: network.id });
} catch (error: any) {
console.error('Failed to create network:', error);
res.status(500).json({ error: error.message || 'Failed to create network' });
}
});
// --- App Templates Routes ---
app.get('/api/templates', async (req: Request, res: Response) => {