From c5ff8418ac04163af11a74e67c6c0b657eb6f3c4 Mon Sep 17 00:00:00 2001 From: oddlama Date: Mon, 18 Mar 2024 21:13:37 +0100 Subject: [PATCH] feat(topology): add html card templating and svg rendering --- pkgs/default.nix | 1 + pkgs/html-to-svg/default.nix | 22 +++ pkgs/html-to-svg/main.js | 51 ++++++ pkgs/html-to-svg/package-lock.json | 201 +++++++++++++++++++++ pkgs/html-to-svg/package.json | 17 ++ topology/icons/interfaces/wifi.svg | 2 + topology/icons/services/openssh.svg | 10 + topology/icons/services/vaultwarden.svg | 7 + topology/nixos/extractors/services.nix | 23 ++- topology/options/interfaces.nix | 22 ++- topology/options/services.nix | 29 ++- topology/topology/renderers/d2/default.nix | 3 +- topology/topology/renderers/d2/network.nix | 163 +++++++++++++++-- 13 files changed, 523 insertions(+), 28 deletions(-) create mode 100644 pkgs/html-to-svg/default.nix create mode 100644 pkgs/html-to-svg/main.js create mode 100644 pkgs/html-to-svg/package-lock.json create mode 100644 pkgs/html-to-svg/package.json create mode 100644 topology/icons/interfaces/wifi.svg create mode 100644 topology/icons/services/openssh.svg create mode 100644 topology/icons/services/vaultwarden.svg diff --git a/pkgs/default.nix b/pkgs/default.nix index 5ebe973..0c25de4 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -25,6 +25,7 @@ segoe-ui-ttf = prev.callPackage ./segoe-ui-ttf.nix {}; zsh-histdb-skim = prev.callPackage ./zsh-skim-histdb.nix {}; awakened-poe-trade = prev.callPackage ./awakened-poe-trade.nix {}; + html-to-svg = prev.callPackage ./html-to-svg {}; neovim-clean = prev.neovim-unwrapped.overrideAttrs (old: { nativeBuildInputs = (old.nativeBuildInputs or []) ++ [prev.makeWrapper]; postInstall = diff --git a/pkgs/html-to-svg/default.nix b/pkgs/html-to-svg/default.nix new file mode 100644 index 0000000..201954d --- /dev/null +++ b/pkgs/html-to-svg/default.nix @@ -0,0 +1,22 @@ +{ + buildNpmPackage, + lib, +}: +buildNpmPackage rec { + pname = "html-to-svg"; + version = "1.0.0"; + + src = ./.; + npmDepsHash = "sha256-0gm43QSUBg219ueFuNSjz857Y1OttSKFc4VltXF78yg="; + dontNpmBuild = true; + + #passthru.updateScript = nix-update-script { }; + + meta = with lib; { + description = "Convert satori compatible HTML to SVG"; + #homepage = "https://github.com/oddlama/html-to-svg"; + license = licenses.mit; + maintainers = with maintainers; [oddlama]; + mainProgram = "html-to-svg"; + }; +} diff --git a/pkgs/html-to-svg/main.js b/pkgs/html-to-svg/main.js new file mode 100644 index 0000000..f250060 --- /dev/null +++ b/pkgs/html-to-svg/main.js @@ -0,0 +1,51 @@ +import fs from "node:fs/promises"; +import satori from "satori"; +import { html } from "satori-html"; +import { Command } from "commander"; + +const program = new Command(); + +program + .name("html-to-svg") + .description("Convert satori compatible HTML to SVG") + .version("1.0.0") + .argument("", "satori html file to render") + .argument("", "output svg") + .option("--font ", "Sets the font") + .option("--font-bold ", "Sets the bold font") + .option("-w, --width ", "Sets the width of the output", 680) + .action(async (input, output, options) => { + if (!options.font) { + console.error("--font is required"); + process.exit(1); + } + + if (!options.fontBold) { + console.error("--font-bold is required"); + process.exit(1); + } + + const markup = html(await fs.readFile(input, { encoding: "utf8" })); + const svg = await satori(markup, { + width: options.width, + embedFont: true, + fonts: [ + { + name: "Font", + data: await fs.readFile(options.font), + weight: 400, + style: "normal", + }, + { + name: "Font", + data: await fs.readFile(options.fontBold), + weight: 700, + style: "normal", + }, + ], + }); + + await fs.writeFile(output, svg); + }); + +program.parse(); diff --git a/pkgs/html-to-svg/package-lock.json b/pkgs/html-to-svg/package-lock.json new file mode 100644 index 0000000..24c62ed --- /dev/null +++ b/pkgs/html-to-svg/package-lock.json @@ -0,0 +1,201 @@ +{ + "name": "render-svg", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "render-svg", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "commander": "^12.0.0", + "satori": "^0.10.13", + "satori-html": "^0.3.2" + } + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==" + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/satori": { + "version": "0.10.13", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.10.13.tgz", + "integrity": "sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ==", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-to-react-native": "^3.0.0", + "emoji-regex": "^10.2.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-wasm-web": "^0.3.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satori-html": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz", + "integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==", + "dependencies": { + "ultrahtml": "^1.2.0" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==" + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "node_modules/ultrahtml": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", + "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==" + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==" + } + } +} diff --git a/pkgs/html-to-svg/package.json b/pkgs/html-to-svg/package.json new file mode 100644 index 0000000..4dddc55 --- /dev/null +++ b/pkgs/html-to-svg/package.json @@ -0,0 +1,17 @@ +{ + "name": "html-to-svg", + "version": "1.0.0", + "description": "Convert satori compatible HTML to SVG", + "main": "main.js", + "type": "module", + "author": "oddlama ", + "license": "MIT", + "bin": { + "html-to-svg": "./main.js" + }, + "dependencies": { + "commander": "^12.0.0", + "satori": "^0.10.13", + "satori-html": "^0.3.2" + } +} diff --git a/topology/icons/interfaces/wifi.svg b/topology/icons/interfaces/wifi.svg new file mode 100644 index 0000000..4beae0d --- /dev/null +++ b/topology/icons/interfaces/wifi.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/topology/icons/services/openssh.svg b/topology/icons/services/openssh.svg new file mode 100644 index 0000000..ce9ebaf --- /dev/null +++ b/topology/icons/services/openssh.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/topology/icons/services/vaultwarden.svg b/topology/icons/services/vaultwarden.svg new file mode 100644 index 0000000..5a96508 --- /dev/null +++ b/topology/icons/services/vaultwarden.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/topology/nixos/extractors/services.nix b/topology/nixos/extractors/services.nix index 1d90542..3302676 100644 --- a/topology/nixos/extractors/services.nix +++ b/topology/nixos/extractors/services.nix @@ -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}";}; + }; }; } diff --git a/topology/options/interfaces.nix b/topology/options/interfaces.nix index d1d2dd0..7679f9e 100644 --- a/topology/options/interfaces.nix +++ b/topology/options/interfaces.nix @@ -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: { diff --git a/topology/options/services.nix b/topology/options/services.nix index 9d5e65f..24918b3 100644 --- a/topology/options/services.nix +++ b/topology/options/services.nix @@ -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}"; + } + ] + ) + )); + }; } diff --git a/topology/topology/renderers/d2/default.nix b/topology/topology/renderers/d2/default.nix index 67b4107..9e7e77a 100644 --- a/topology/topology/renderers/d2/default.nix +++ b/topology/topology/renderers/d2/default.nix @@ -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 ''; } diff --git a/topology/topology/renderers/d2/network.nix b/topology/topology/renderers/d2/network.nix index 2452dda..b2fec5a 100644 --- a/topology/topology/renderers/d2/network.nix +++ b/topology/topology/renderers/d2/network.nix @@ -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 '' +
+ '' + else if hasSuffix ".svg" file + then let + withoutPrefix = head (tail (splitString "" withoutPrefix); + in '''' + else if hasSuffix ".png" file + # FIXME: TODO png, jpg, ... + then '' + " + '' + 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 + */ + '' +
+
+
+ ${name} +
+
+
+ ''; netToD2 = net: '' ${net.id}: ${net.name} { @@ -56,13 +79,117 @@ # ${node.id}.${interface.id} -- ${x.node}.${x.interface} #'')); + nodeInterfaceHtmlSpacing = '' +
+ ''; + nodeInterfaceHtml = interface: let + color = + if interface.virtual + then "#242931" + else "#70a5eb"; + in + /* + html + */ + '' +
+
+
+ ${mkImage "w-6 h-6 mr-2" (getIcon "interfaces" interface.icon)} + ${interface.id} +
+ addrs: ${toString interface.addresses} +
+ ''; + + nodeServiceDetailsHeader = + /* + html + */ + '' +
+ ''; + + nodeServiceDetail = detail: + /* + html + */ + '' +
+ ${detail.name} + ${detail.text} +
+ ''; + + nodeServiceDetails = service: + optionalString (service.details != {}) nodeServiceDetailsHeader + # FIXME: order not respected + + concatLines ((map nodeServiceDetail) (attrValues service.details)); + + nodeServiceHtml = service: + /* + html + */ + '' +
+
+ ${mkImage "w-16 h-16 mr-4 rounded-lg" (getIcon "services" service.icon)} +
+

${service.name}

+ ${optionalString (service.info != "") ''

${service.info}

''} +
+
+ ${nodeServiceDetails service} +
+ ''; + + nodeHtml = node: let + services = filter (x: !x.hidden) (attrValues node.services); + in + /* + html + */ + '' +
+
+
+

${node.name}

+
+

${node.type}

+
+ + ${optionalString (node.interfaces != {}) (mkSpacer "Interfaces" + nodeInterfaceHtmlSpacing)} + ${concatLines (map nodeInterfaceHtml (attrValues node.interfaces))} + ${optionalString (node.interfaces != {}) nodeInterfaceHtmlSpacing} + + ${optionalString (services != []) (mkSpacer "Services")} + ${concatLines (map nodeServiceHtml services)} + +
+
+
+ ''; + 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))} ''