1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-10 14:50:40 +02:00

refactor(topology): move html to svg to own renderer

This commit is contained in:
oddlama 2024-03-18 23:06:47 +01:00
parent c5ff8418ac
commit 23564f980e
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
7 changed files with 235 additions and 154 deletions

View file

@ -16,12 +16,10 @@
substituters = [
"https://cache.nixos.org"
"https://nix-community.cachix.org"
"https://nix-config.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
"nix-config.cachix.org-1:Vd6raEuldeIZpttVQfrUbLvXJHzzzkS0pezXCVVjDG4="
];
cores = 0;
max-jobs = "auto";

View file

@ -2,7 +2,7 @@
buildNpmPackage,
lib,
}:
buildNpmPackage rec {
buildNpmPackage {
pname = "html-to-svg";
version = "1.0.0";

View file

@ -35,7 +35,7 @@ in
hidden = mkOption {
description = "Whether this service should be hidden from graphs";
default = true;
default = false;
type = types.bool;
};

View file

@ -57,6 +57,14 @@ in {
defaultText = literalExpression ''config.renderers.${config.renderer}.output'';
};
lib = lib.mkOption {
default = {};
type = lib.types.attrsOf lib.types.attrs;
description = lib.mdDoc ''
This option allows modules to define helper functions, constants, etc.
'';
};
assertions = mkOption {
internal = true;
default = [];

View file

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

View file

@ -8,50 +8,8 @@
(lib)
attrValues
concatLines
filter
hasSuffix
head
optionalString
splitString
tail
;
getIcon = registry: iconName:
if iconName == null
then null
else config.icons.${registry}.${iconName}.file or null;
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}";
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} {
info: |md
@ -79,117 +37,13 @@
# ${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.runCommand "generate-node-svgs" {} ''
mkdir -p $out
${concatLines (map generateNodeSvg (attrValues config.nodes))}
pkgs.writeText "network.d2" ''
${concatLines (map netToD2 (attrValues config.networks))}
${concatLines (map nodeToD2 (attrValues config.nodes))}
''

View file

@ -0,0 +1,221 @@
{
config,
lib,
pkgs,
...
}: let
inherit
(lib)
attrValues
concatLines
filter
flip
hasSuffix
head
mkOption
optionalString
splitString
tail
types
;
htmlToSvgCommand = inFile: outFile: ''
${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 \
${inFile} ${outFile}
'';
renderHtmlToSvg = html: name: let
drv = pkgs.runCommand "generate-svg-${name}" {} ''
mkdir -p $out
${htmlToSvgCommand (pkgs.writeText "${name}.html" html) "$out/${name}.svg"}
'';
in "${drv}/${name}.svg";
getIcon = registry: iconName:
if iconName == null
then null
else config.icons.${registry}.${iconName}.file or null;
html = rec {
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}";
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>
'';
mkRootContainer = contents:
/*
html
*/
''
<div tw="flex flex-col w-full h-full items-center">
${contents}
</div>
'';
mkRootCard = twAttrs: contents:
mkRootContainer
/*
html
*/
''
<div tw="flex flex-col w-full h-full bg-[#101419] text-[#e3e6eb] font-mono ${twAttrs}" style="font-family: 'JetBrains Mono'">
${contents}
</div>
'';
spacingMt2 = ''
<div tw="flex mt-2"></div>
'';
node = rec {
mkInterface = 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>
'';
serviceDetail = 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>
'';
serviceDetails = service:
optionalString (service.details != {}) ''<div tw="flex pt-2"></div>''
# FIXME: order not respected
+ concatLines ((map serviceDetail) (attrValues service.details));
mkService = 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>
${serviceDetails service}
</div>
'';
mkTitle = node:
/*
html
*/
''
<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">${node.type}</h2>
</div>
'';
mkInfoCardTitle = node:
mkRootCard "rounded-t-xl"
/*
html
*/
''
${mkTitle node}
'';
mkInfoCardFull = node: let
services = filter (x: !x.hidden) (attrValues node.services);
in
mkRootCard "rounded-xl"
/*
html
*/
''
${mkTitle node}
${optionalString (node.interfaces != {}) (mkSpacer "Interfaces" + spacingMt2)}
${concatLines (map mkInterface (attrValues node.interfaces))}
${optionalString (node.interfaces != {}) spacingMt2}
${optionalString (services != []) (mkSpacer "Services")}
${concatLines (map mkService services)}
<div tw="flex mb-2"></div>
'';
};
};
in {
options.renderers.svg = {
# FIXME: colors.bg0 = mkColorOption "bg0" "#";
output = mkOption {
description = "The derivation containing the rendered output";
type = types.path;
readOnly = true;
};
};
config = {
lib.renderers.svg.node = {
mkInfoCardFull = node: renderHtmlToSvg (html.node.mkInfoCardFull node) node.name;
};
renderers.svg.output = pkgs.runCommand "topology-svgs" {} ''
mkdir -p $out/nodes
${concatLines (flip map (attrValues config.nodes) (
node:
htmlToSvgCommand (
pkgs.writeText "node-${node.name}.html" (html.node.mkInfoCardFull node)
) "$out/nodes/${node.name}.svg"
))}
'';
};
}