feat(topology): add html card templating and svg rendering

This commit is contained in:
oddlama 2024-03-18 21:13:37 +01:00
parent 11562ead05
commit c5ff8418ac
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
13 changed files with 523 additions and 28 deletions

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 -5 34 34" xmlns="http://www.w3.org/2000/svg"><path d="m29.294 7.765c-.572.016-1.03.484-1.03 1.059s.458 1.043 1.029 1.059h.002c.572-.016 1.03-.484 1.03-1.059s-.458-1.043-1.029-1.059zm-1.059 7.412v.029c0 .585.474 1.059 1.059 1.059s1.059-.474 1.059-1.059c0-.01 0-.02 0-.031v.001-3.53c0-.009 0-.019 0-.029 0-.585-.474-1.059-1.059-1.059s-1.059.474-1.059 1.059v.031-.002zm-2.47-7.412c.572.016 1.03.484 1.03 1.059s-.458 1.043-1.029 1.059h-.002-3.882v1.412h2.47c.572.016 1.03.484 1.03 1.059s-.458 1.043-1.029 1.059h-.002-2.47v1.765.029c0 .585-.474 1.059-1.059 1.059s-1.059-.474-1.059-1.059c0-.01 0-.02 0-.031v.002-6.354c0-.001 0-.003 0-.005 0-.582.472-1.054 1.054-1.054h.005zm8.117 2.117v4.235c0 3.119-2.529 5.647-5.647 5.647h-2.118c-2.225 2.599-5.51 4.235-9.176 4.235s-6.951-1.636-9.163-4.219l-.014-.016h-2.118c-3.119 0-5.647-2.529-5.647-5.647v-4.235c0-3.119 2.529-5.647 5.647-5.647h2.118c2.225-2.599 5.51-4.235 9.176-4.235s6.951 1.636 9.163 4.219l.014.016h2.118c3.119 0 5.647 2.529 5.647 5.647zm-19.412-2.117c-.572.016-1.03.484-1.03 1.059s.458 1.043 1.029 1.059h.001c.572-.016 1.03-.484 1.03-1.059s-.458-1.043-1.029-1.059h-.002zm-1.059 7.412c.016.572.484 1.03 1.059 1.03s1.043-.458 1.059-1.029v-.002-3.53c-.016-.572-.484-1.03-1.059-1.03s-1.043.458-1.059 1.029v.002zm-3.177.117 1.647-6c.068-.136.107-.297.107-.466 0-.589-.478-1.067-1.067-1.067-.504 0-.927.35-1.039.821l-.001.007-.706 2.588-.706-2.588c-.138-.462-.56-.793-1.059-.793s-.92.331-1.057.786l-.002.008-.706 2.588-.706-2.588c-.124-.46-.538-.794-1.029-.794-.588 0-1.064.476-1.064 1.064 0 .158.034.307.096.442l-.003-.007 1.647 6c.072.527.518.928 1.059.928s.987-.402 1.058-.923l.001-.006.706-2.47.706 2.47c.051.544.506.967 1.059.967s1.008-.422 1.058-.962v-.004zm18-9.647h-7.411c-.004 0-.009 0-.014 0-1.747 0-3.163 1.416-3.163 3.163v.014-.001 6.353.019c0 1.669-1.292 3.037-2.93 3.157l-.01.001h13.53.018c2.329 0 4.218-1.888 4.218-4.218 0-.006 0-.012 0-.019v.001-4.236c0-.005 0-.011 0-.018 0-2.329-1.888-4.218-4.218-4.218-.006 0-.012 0-.019 0h.001z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="-9.312 -18.987 141.495 141.495" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style>
</defs>
<rect x="-9.312" y="-18.987" width="141.495" height="141.495" style="fill: rgb(235, 235, 235);"/>
<g>
<path class="st0" d="M5.47,0h111.93c3.01,0,5.47,2.46,5.47,5.47v92.58c0,3.01-2.46,5.47-5.47,5.47H5.47 c-3.01,0-5.47-2.46-5.47-5.47V5.47C0,2.46,2.46,0,5.47,0L5.47,0z M31.84,38.55l17.79,18.42l2.14,2.13l-2.12,2.16L31.68,80.31 l-5.07-5l15.85-16.15L26.81,43.6L31.84,38.55L31.84,38.55z M94.1,79.41H54.69v-6.84H94.1V79.41L94.1,79.41z M38.19,9.83 c3.19,0,5.78,2.59,5.78,5.78s-2.59,5.78-5.78,5.78c-3.19,0-5.78-2.59-5.78-5.78S35,9.83,38.19,9.83L38.19,9.83z M18.95,9.83 c3.19,0,5.78,2.59,5.78,5.78s-2.59,5.78-5.78,5.78c-3.19,0-5.78-2.59-5.78-5.78S15.75,9.83,18.95,9.83L18.95,9.83z M7.49,5.41 h107.91c1.15,0,2.09,0.94,2.09,2.09v18.32H5.4V7.5C5.4,6.35,6.34,5.41,7.49,5.41L7.49,5.41z" style=""/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1,023 B

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="imgur" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#175DDC"/><path fill="#ffffff" d="M372 297V131H256v294c47-28 115-74 116-128zm49-198v198c0 106-152 181-165 181S91 403 91 297V99s0-17 17-17h296s17 0 17 17z"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View file

@ -5,14 +5,29 @@
}: let
inherit
(lib)
concatStringsSep
mkDefault
mkIf
;
in {
topology.self.services = {
vaultwarden = mkIf config.services.vaultwarden.enable {
name = "Vaultwarden";
info = "https://pw.example.com";
details.listen.text = "[::]:3000";
openssh = mkIf config.services.openssh.enable {
hidden = mkDefault true; # Causes a lot of much clutter
name = "OpenSSH";
icon = "openssh";
info = "port: ${concatStringsSep ", " (map toString config.services.openssh.ports)}";
};
vaultwarden = let
domain = config.services.vaultwarden.config.domain or config.services.vaultwarden.config.DOMAIN or null;
address = config.services.vaultwarden.config.rocketAddress or config.services.vaultwarden.config.ROCKET_ADDRESS or null;
port = config.services.vaultwarden.config.rocketPort or config.services.vaultwarden.config.ROCKET_PORT or null;
in
mkIf config.services.vaultwarden.enable {
name = "Vaultwarden";
icon = "vaultwarden";
info = mkIf (domain != null) domain;
details.listen = mkIf (address != null && port != null) {text = "${address}:${toString port}";};
};
};
}

View file

@ -8,6 +8,7 @@ f: {
attrValues
flatten
flip
mkDefault
mkOption
types
;
@ -47,9 +48,8 @@ in
};
icon = mkOption {
description = "The icon for this interface. If null, an icon will be selected from `icons.interfaces` based on the specified type.";
default = null;
type = types.nullOr types.path;
description = "The icon for this interface. Must be a valid entry in icons.interfaces. If null, an icon will be selected based on the type.";
type = types.nullOr types.str;
};
addresses = mkOption {
@ -88,6 +88,18 @@ in
});
};
};
config = {
icon = mkDefault (
{
ethernet = "ethernet";
wireguard = "wireguard";
wifi = "wifi";
}
.${submod.config.type}
or null
);
};
}));
};
};
@ -104,6 +116,10 @@ in
assertion = interface.network != null -> config.networks ? ${interface.network};
message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown network '${interface.network}'";
}
{
assertion = interface.icon != null -> config.icons.interfaces ? ${interface.icon};
message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown icon icons.interfaces.${interface.icon}";
}
]
++ flip map interface.physicalConnections (
physicalConnection: {

View file

@ -5,6 +5,9 @@ f: {
}: let
inherit
(lib)
attrValues
flatten
flip
mkOption
types
;
@ -30,19 +33,27 @@ in
type = types.str;
};
hidden = mkOption {
description = "Whether this service should be hidden from graphs";
default = true;
type = types.bool;
};
icon = mkOption {
description = "The icon for this service";
type = types.nullOr types.path;
description = "The icon for this service. Must be a valid entry in icons.services.";
type = types.nullOr types.str;
default = null;
};
info = mkOption {
description = "Additional high-profile information about this service, usually the url or listen address. Most likely shown directly below the name.";
default = "";
type = types.lines;
};
details = mkOption {
description = "Additional detail sections that should be shown to the user.";
default = {};
type = types.attrsOf (types.submodule (detailSubmod: {
options = {
name = mkOption {
@ -71,4 +82,18 @@ in
};
});
};
config = {
assertions = flatten (flip map (attrValues config.nodes) (
node:
flip map (attrValues node.services) (
service: [
{
assertion = service.icon != null -> config.icons.services ? ${service.icon};
message = "topology: nodes.${node.id}.services.${service.id} refers to an unknown icon icons.services.${service.icon}";
}
]
)
));
};
}

View file

@ -19,6 +19,7 @@ in {
config.renderers.d2.output = pkgs.runCommand "build-d2-topology" {} ''
mkdir -p $out
cp ${import ./network.nix args} $out/network.d2
# cp ${import ./network.nix args} $out/network.d2
ln -s ${import ./network.nix args} $out/svgs
'';
}

View file

@ -8,26 +8,49 @@
(lib)
attrValues
concatLines
filter
hasSuffix
head
optionalString
splitString
tail
;
#toD2 = _nodeName: node: ''
# ${node.id}: |md
# # ${node.id}
getIcon = registry: iconName:
if iconName == null
then null
else config.icons.${registry}.${iconName}.file or null;
# ## Disks:
# ${concatLines (mapAttrsToList (_: v: "- ${v.id}") node.disks)}
mkImage = twAttrs: file:
if file == null
then ''
<div tw="flex flex-none bg-[#000000] ${twAttrs}"></div>
''
else if hasSuffix ".svg" file
then let
withoutPrefix = head (tail (splitString "<svg " (builtins.readFile file)));
content = head (splitString "</svg>" withoutPrefix);
in ''<svg tw="${twAttrs}" ${content}</svg>''
else if hasSuffix ".png" file
# FIXME: TODO png, jpg, ...
then ''
<img tw="${twAttrs}" src="data:image/png;base64,${"TODO"}/>"
''
else builtins.throw "Unsupported icon file type: ${file}";
# ## Interfaces:
# ${concatLines (mapAttrsToList (_: v: "- ${v.id}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.interfaces)}
# ## Firewall Zones:
# ${concatLines (mapAttrsToList (_: v: "- ${v.id}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.firewallRules)}
# ## Services:
# ${concatLines (mapAttrsToList (_: v: "- ${v.id}, name ${toString v.name}, icon ${toString v.icon}, url ${toString v.url}") node.services)}
# |
#'';
mkSpacer = name:
/*
html
*/
''
<div tw="flex flex-row w-full items-center">
<div tw="flex grow h-0.5 my-4 bg-[#242931] border-0"></div>
<div tw="flex px-4">
<span tw="text-[#b6beca] font-bold">${name}</span>
</div>
<div tw="flex grow h-0.5 my-4 bg-[#242931] border-0"></div>
</div>
'';
netToD2 = net: ''
${net.id}: ${net.name} {
@ -56,13 +79,117 @@
# ${node.id}.${interface.id} -- ${x.node}.${x.interface}
#''));
nodeInterfaceHtmlSpacing = ''
<div tw="flex mt-2"></div>
'';
nodeInterfaceHtml = interface: let
color =
if interface.virtual
then "#242931"
else "#70a5eb";
in
/*
html
*/
''
<div tw="flex flex-row items-center my-2">
<div tw="flex flex-row flex-none bg-[${color}] w-4 h-1"></div>
<div tw="flex flex-row flex-none items-center bg-[${color}] text-[#101419] rounded-lg px-2 py-1 w-46 h-8 mr-4">
${mkImage "w-6 h-6 mr-2" (getIcon "interfaces" interface.icon)}
<span tw="font-bold">${interface.id}</span>
</div>
<span>addrs: ${toString interface.addresses}</span>
</div>
'';
nodeServiceDetailsHeader =
/*
html
*/
''
<div tw="flex pt-2"></div>
'';
nodeServiceDetail = detail:
/*
html
*/
''
<div tw="flex flex-row mt-1">
<span tw="flex flex-none w-20 font-bold pl-1">${detail.name}</span>
<span tw="flex grow">${detail.text}</span>
</div>
'';
nodeServiceDetails = service:
optionalString (service.details != {}) nodeServiceDetailsHeader
# FIXME: order not respected
+ concatLines ((map nodeServiceDetail) (attrValues service.details));
nodeServiceHtml = service:
/*
html
*/
''
<div tw="flex flex-col mx-4 mt-4 bg-[#21262e] rounded-lg p-2">
<div tw="flex flex-row items-center">
${mkImage "w-16 h-16 mr-4 rounded-lg" (getIcon "services" service.icon)}
<div tw="flex flex-col grow">
<h1 tw="text-xl font-bold m-0">${service.name}</h1>
${optionalString (service.info != "") ''<p tw="text-base m-0">${service.info}</p>''}
</div>
</div>
${nodeServiceDetails service}
</div>
'';
nodeHtml = node: let
services = filter (x: !x.hidden) (attrValues node.services);
in
/*
html
*/
''
<div tw="flex flex-col w-full h-full items-center font-mono text-[#e3e6eb]" style="font-family: 'JetBrains Mono'">
<div tw="flex flex-col w-full h-full bg-[#101419] py-2 rounded-xl">
<div tw="flex flex-row mx-6 my-2">
<h2 tw="grow text-4xl font-bold">${node.name}</h2>
<div tw="flex grow"></div>
<h2 tw="text-4xl" style="font-family: 'Segoe UI Emoji'">${node.type}</h2>
</div>
${optionalString (node.interfaces != {}) (mkSpacer "Interfaces" + nodeInterfaceHtmlSpacing)}
${concatLines (map nodeInterfaceHtml (attrValues node.interfaces))}
${optionalString (node.interfaces != {}) nodeInterfaceHtmlSpacing}
${optionalString (services != []) (mkSpacer "Services")}
${concatLines (map nodeServiceHtml services)}
<div tw="flex mb-2"></div>
</div>
</div>
'';
nodeToD2 = node: ''
${node.id}: ${node.name} {}
${concatLines (map (nodeInterfaceToD2 node) (attrValues node.interfaces))}
'';
generateNodeSvg = node: ''
${lib.getExe pkgs.html-to-svg} \
--font ${pkgs.jetbrains-mono}/share/fonts/truetype/JetBrainsMono-Regular.ttf \
--font-bold ${pkgs.jetbrains-mono}/share/fonts/truetype/JetBrainsMono-Bold.ttf \
--width 680 \
${pkgs.writeText "${node.name}.html" (nodeHtml node)} \
$out/${node.name}.svg
'';
in
pkgs.writeText "network.d2" ''
${concatLines (map netToD2 (attrValues config.networks))}
${concatLines (map nodeToD2 (attrValues config.nodes))}
#pkgs.writeText "network.d2" ''
# ${concatLines (map netToD2 (attrValues config.networks))}
# ${concatLines (map nodeToD2 (attrValues config.nodes))}
#''
pkgs.runCommand "generate-node-svgs" {} ''
mkdir -p $out
${concatLines (map generateNodeSvg (attrValues config.nodes))}
''