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:
parent
c5ff8418ac
commit
23564f980e
7 changed files with 235 additions and 154 deletions
|
@ -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";
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
buildNpmPackage,
|
||||
lib,
|
||||
}:
|
||||
buildNpmPackage rec {
|
||||
buildNpmPackage {
|
||||
pname = "html-to-svg";
|
||||
version = "1.0.0";
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ in
|
|||
|
||||
hidden = mkOption {
|
||||
description = "Whether this service should be hidden from graphs";
|
||||
default = true;
|
||||
default = false;
|
||||
type = types.bool;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))}
|
||||
''
|
||||
|
|
221
topology/topology/renderers/svg/default.nix
Normal file
221
topology/topology/renderers/svg/default.nix
Normal 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"
|
||||
))}
|
||||
'';
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue