mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-10 23:00:39 +02:00
feat(topology): add html card templating and svg rendering
This commit is contained in:
parent
11562ead05
commit
c5ff8418ac
13 changed files with 523 additions and 28 deletions
2
topology/icons/interfaces/wifi.svg
Normal file
2
topology/icons/interfaces/wifi.svg
Normal 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 |
10
topology/icons/services/openssh.svg
Normal file
10
topology/icons/services/openssh.svg
Normal 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 |
7
topology/icons/services/vaultwarden.svg
Normal file
7
topology/icons/services/vaultwarden.svg
Normal 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 |
|
@ -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}";};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
]
|
||||
)
|
||||
));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -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))}
|
||||
''
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue