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

feat(topology): add automatic lazy network propagation

This commit is contained in:
oddlama 2024-03-31 17:19:26 +02:00
parent 65f1fc4bd7
commit dc4d82c828
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
9 changed files with 412 additions and 137 deletions

11
topology/README.md Normal file
View file

@ -0,0 +1,11 @@
## Options
## Renderers
### svg
### elk
## NixOS Extractors
## Network propagation

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" viewBox="0 0 512 512"><path d="M168 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M256 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M212 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/><path d="M460 0H52C23.28 0 0 23.28 0 52v408c0 28.72 23.28 52 52 52h408c28.72 0 52-23.28 52-52V52c0-28.72-23.28-52-52-52m-16 284h-88.024c-2.212 0-3.976 1.64-3.976 3.848V348H160v-60.152c0-2.208-1.616-3.848-3.828-3.848H68V68h44v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h44z"/><path d="M124 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M388 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M344 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M300 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" viewBox="0 0 512 512"><path fill="#e3e6eb" d="M168 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M256 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M212 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/><path fill="#e3e6eb" d="M460 0H52C23.28 0 0 23.28 0 52v408c0 28.72 23.28 52 52 52h408c28.72 0 52-23.28 52-52V52c0-28.72-23.28-52-52-52m-16 284h-88.024c-2.212 0-3.976 1.64-3.976 3.848V348H160v-60.152c0-2.208-1.616-3.848-3.828-3.848H68V68h44v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h44z"/><path fill="#e3e6eb" d="M124 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M388 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M344 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M300 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

@ -13,7 +13,6 @@
mkIf
mkMerge
filter
optionals
;
headOrNull = xs:
@ -49,7 +48,6 @@ in {
wgName: wgCfg: let
inherit
(lib.wireguard inputs wgName)
participatingClientNodes
participatingServerNodes
wgCfgOf
;
@ -57,11 +55,6 @@ in {
isServer = wgCfg.server.host != null;
filterSelf = filter (x: x != config.node.name);
# All nodes that use our node as the via into the wireguard network
ourClientNodes =
optionals isServer
(filter (n: (wgCfgOf n).client.via == config.node.name) participatingClientNodes);
# The list of peers that are "physically" connected in the wireguard network,
# meaning they communicate directly with each other.
connectedPeers =
@ -69,13 +62,12 @@ in {
then
# Other servers in the same network
filterSelf participatingServerNodes
# Our clients
++ ourClientNodes
else [wgCfg.client.via];
in {
${wgCfg.linkName} = {
network = networkId wgName;
virtual = true;
renderer.hidePhysicalConnections = true;
physicalConnections = flip map connectedPeers (peer: {
node = inputs.self.nodes.${peer}.config.topology.id;
interface = (wgCfgOf peer).linkName;

View file

@ -1,23 +1,231 @@
f: {
lib,
config,
options,
...
}: let
inherit
(lib)
all
assertMsg
attrNames
attrValues
concatLines
concatMap
concatStringsSep
const
elem
flatten
flip
foldl'
isAttrs
length
mapAttrsToList
mkDefault
mkIf
mkOption
mkOptionType
optional
optionalAttrs
recursiveUpdate
reverseList
showOption
types
unique
warnIf
;
# Checks whether the value is a lazy value without causing
# it's value to be evaluated
isLazyValue = x: isAttrs x && x ? _lazyValue;
# Constructs a lazy value holding the given value.
lazyValue = value: {_lazyValue = value;};
# Represents a lazy value of the given type, which
# holds the actual value as an attrset like { _lazyValue = <actual value>; }.
# This allows the option to be defined and filtered from a defintion
# list without evaluating the value.
lazyValueOf = type:
mkOptionType rec {
name = "lazyValueOf ${type.name}";
inherit (type) description descriptionClass emptyValue getSubOptions getSubModules;
check = isLazyValue;
merge = loc: defs:
assert assertMsg
(all (x: type.check x._lazyValue) defs)
"The option `${showOption loc}` is defined with a lazy value holding an invalid type";
types.mergeOneOption loc defs;
substSubModules = m: types.uniq (type.substSubModules m);
functor = (types.defaultFunctor name) // {wrapped = type;};
nestedTypes.elemType = type;
};
# Represents a value or lazy value of the given type that will
# automatically be coerced to the given type when merged.
lazyOf = type: types.coercedTo (lazyValueOf type) (x: x._lazyValue) type;
allNodes = attrNames config.nodes;
allInterfacesOf = node: attrNames config.nodes.${node}.interfaces;
allNodeInterfacePairs = concatMap (node: map (interface: {inherit node interface;}) (allInterfacesOf node)) allNodes;
# Helper to add a value to a list if it doesn't already hold it
addIfNotExists = list: x:
if elem x list
then list
else list ++ [x];
# The list of networks that were specifically assigned by the user
# to other interfaces which are sharing their network with us.
networkDefiningInterfaces = foldl' recursiveUpdate {} (flatten (
flip map options.nodes.definitions (mapAttrsToList (
nodeId: node:
flip mapAttrsToList (node.interfaces or {}) (
interfaceId: interface:
optional (interface ? network && !isLazyValue interface.network) {
${nodeId}.${interfaceId} = interface.network;
}
)
))
));
# A list of all connections between all interfaces. Bidirectional.
connectionList = unique (flatten (flip mapAttrsToList config.nodes (
nodeId: node:
flip mapAttrsToList node.interfaces (
interfaceId: interface:
flip map interface.physicalConnections (conn: [
{
src = {
node = nodeId;
interface = interfaceId;
};
dst = conn;
}
{
src = conn;
dst = {
node = nodeId;
interface = interfaceId;
};
}
])
)
)));
# A map of all connections constructed from the connection list
connections = foldl' (acc: {
src,
dst,
}:
recursiveUpdate acc {
${src.node}.${src.interface} = addIfNotExists (acc.${src.node}.${src.interface} or []) dst;
}) {}
connectionList;
# Propagates all networks from the source interface to the destination interface
propagateNetworks = state: snode: sinterface: dnode: dinterface:
#builtins.trace " propagate all nets (${toString (attrNames state.${snode}.${sinterface})}) of ${snode}.${sinterface} to ${dnode}.${dinterface}"
(
# Fold over each network that the source interface shares
# and propagate it if the destination doesn't already have this network
foldl' (
acc: net:
recursiveUpdate acc (
optionalAttrs (!(acc ? ${dnode}.${dinterface}.${net}))
#builtins.trace " adding ${net} to ${dnode}.${dinterface} via ${toString (acc.${snode}.${sinterface}.${net} ++ ["${snode}.${sinterface}"])}"
{
# Create the network on the destination interface and append our interface
# to the list which indicates from where the network was received
${dnode}.${dinterface}.${net} = acc.${snode}.${sinterface}.${net} ++ ["${snode}.${sinterface}"];
}
)
)
state (attrNames state.${snode}.${sinterface})
);
# Propagates all shared networks for the given interface to other interfaces connected to it
propagateToConnections = state: src:
#builtins.trace "propagate via connections of ${src.node}.${src.interface}"
(
# Fold over each connection of the current interface
foldl' (
acc: dst:
propagateNetworks acc src.node src.interface dst.node dst.interface
)
state (connections.${src.node}.${src.interface} or [])
);
# Propagates all shared networks for the given interface to other interfaces on the same
# node to which the network is shared
propagateLocally = state: src:
#builtins.trace "propagate locally on ${src.node}.${src.interface}"
(
# Fold over each local interface of the current node
foldl' (
acc: dstInterface:
if src.interface != dstInterface && config.nodes.${src.node}.interfaces.${src.interface}.sharesNetworkWith dstInterface
then propagateNetworks acc src.node src.interface src.node dstInterface
else acc
)
state (allInterfacesOf src.node)
);
# Assigns each interface a list of networks that were propagated to it
# from another interface or connection.
propagatedNetworks = let
# Initially set sharedNetworks based on whether the interface has a network assigned to it.
# This list will then be expaned iteratively.
initial = foldl' recursiveUpdate {} (flatten (flip map allNodes (
node:
flip map (allInterfacesOf node) (interface: {
${node}.${interface} = optionalAttrs (networkDefiningInterfaces ? ${node}.${interface}) {
${networkDefiningInterfaces.${node}.${interface}} = [];
};
})
)));
# Takes a state and propagates networks via local sharing on the same node
propagateEachLocally = state: foldl' propagateLocally state allNodeInterfacePairs;
# Takes a state and propagates networks via sharing over connections
propagateEachToConnections = state: foldl' propagateToConnections state allNodeInterfacePairs;
# The update function that propagates state from all interfaces to all neighbors.
# The fixpoint of this function is the solution.
update = state: propagateEachToConnections (propagateEachLocally state);
converged = lib.converge update initial;
# Extract all interfaces that were assigned multiple interfaces, to issue a warning
interfacesWithMultipleNets = flatten (
flip mapAttrsToList converged (
node: ifs:
flip mapAttrsToList ifs (
interface: nets:
optional (length (attrNames nets) > 1) {
inherit node interface nets;
}
)
)
);
in
warnIf (interfacesWithMultipleNets != []) ''
topology: Some interfaces have received multiple networks via network propagation!
This is an error in your network configuration that must be addressed.
Evaluation can still continue by considering only one of them (effectively at random).
The affected interfaces are:
${concatLines (flip map interfacesWithMultipleNets (
x:
" - ${x.node}.${x.interface}:\n"
+ concatLines (
flip mapAttrsToList x.nets (
net: assignments: " ${net} assigned via ${concatStringsSep " -> " (reverseList assignments)}"
)
)
))}''
converged;
in
f {
options.nodes = mkOption {
type = types.attrsOf (types.submodule {
type = types.attrsOf (types.submodule (nodeSubmod: {
options = {
interfaces = mkOption {
description = "TODO";
@ -67,10 +275,38 @@ in
type = types.listOf types.str;
};
network = mkOption {
description = "The id of the network to which this interface belongs, if any.";
default = null;
type = types.nullOr types.str;
network =
mkOption {
description = "The id of the network to which this interface belongs, if any.";
type = lazyOf (types.nullOr types.str);
}
// optionalAttrs config.topology.isMainModule {
default = let
sharedNetworks = propagatedNetworks.${nodeSubmod.config.id}.${submod.config.id} or {};
sharedNetwork =
if sharedNetworks == {}
then null
else builtins.head (attrNames sharedNetworks);
in
lazyValue sharedNetwork;
};
sharesNetworkWith = mkOption {
description = ''
Defines a predicate that determines whether this interface shares its connected network with another provided local interface.
The predicates takes the name of another interface and returns true if our network should be shared with the given interface.
Sharing here means that if a network is set on this interface, it will also be set as the network for any
shared interface. Setting the same predicate on multiple interfaces causes them to share a network regardless
on which port the network is actually defined.
An unmanaged switch for example would set this to `const true`, effectively
propagating the network set on one port to all other ports. Having two assigned
networks within one predicate group will cause a warning to be issued.
'';
default = const false;
defaultText = ''const false'';
type = types.functionTo types.bool;
};
physicalConnections = mkOption {
@ -90,6 +326,21 @@ in
};
});
};
# Rendering related hints and settings
renderer = {
hidePhysicalConnections = mkOption {
description = ''
Whether to hide physical connections of this interface in renderings.
Affects both outgoing connections defined here and incoming connections
defined on other interfaces.
Usually only affects rendering of the main topology view, not network-centric views.
'';
type = types.bool;
default = false;
};
};
};
config = {
@ -101,10 +352,13 @@ in
}));
};
};
});
}));
};
config = {
lib.a.a = connections;
lib.a.b = networkDefiningInterfaces;
lib.a.c = propagatedNetworks;
assertions = flatten (flip map (attrValues config.nodes) (
node:
flip map (attrValues node.interfaces) (

View file

@ -41,8 +41,8 @@ in
};
hardware = {
description = mkOption {
description = "A description of this node's hardware. Usually the model name or a description the most important components.";
info = mkOption {
description = "A single line of information about this node's hardware. Usually the model name or a description the most important components.";
type = types.str;
default = "";
};
@ -64,7 +64,7 @@ in
description = ''
The device type of the node. This can be set to anything, but some special
values exist that will automatically set some other defaults, most notably
the deviceIcon and preferredRenderType.
the deviceIcon and renderer.preferredType.
'';
type = types.either (types.enum ["nixos" "internet" "router" "switch" "device"]) types.str;
};
@ -87,15 +87,18 @@ in
type = types.nullOr types.str;
};
preferredRenderType = mkOption {
description = ''
An optional hint to the renderer to specify whether this node should preferrably
rendered as a full card, or just as an image with name. If there is no hardware
image, this will usually still render a small card.
'';
type = types.enum ["card" "image"];
default = "card";
defaultText = ''"card" # defaults to card but is also derived from the deviceType if possible.'';
# Rendering related hints and settings
renderer = {
preferredType = mkOption {
description = ''
An optional hint to the renderer to specify whether this node should preferrably
rendered as a full card, or just as an image with name. If there is no hardware
image, this will usually still render a small card.
'';
type = types.enum ["card" "image"];
default = "card";
defaultText = ''"card" # defaults to card but is also derived from the deviceType if possible.'';
};
};
};
@ -103,16 +106,19 @@ in
nodeCfg = nodeSubmod.config;
in
mkIf config.topology.isMainModule (mkMerge [
# Set the default icon, if an icon exists with a matching name
{
# Set the default icon, if an icon exists with a matching name
deviceIcon = mkIf (config.icons.devices ? ${nodeCfg.deviceType}) (
mkDefault ("devices." + nodeCfg.deviceType)
);
# Set the hardware info to the guest type if nothing else was set
hardware.info = mkIf (nodeCfg.guestType != null) (mkDefault nodeCfg.guestType);
}
# If the device type is not a full nixos node, try to render it as an image with name.
(mkIf (elem nodeCfg.deviceType ["internet" "router" "switch" "device"]) {
preferredRenderType = mkDefault "image";
renderer.preferredType = mkDefault "image";
})
]);
}));

View file

@ -20,7 +20,6 @@
mkOption
optional
optionalAttrs
optionals
recursiveUpdate
stringLength
types
@ -64,6 +63,13 @@
// extra;
};
mkPort = recursiveUpdate {
width = 8;
height = 8;
style.stroke = "#485263";
style.fill = "#b6beca";
};
mkLabel = text: scale: extraStyle: {
height = scale * 12;
width = scale * 7.2 * (stringLength text);
@ -83,82 +89,72 @@
scale = 0.8;
};
properties."portLabels.placement" = "OUTSIDE";
ports.default = mkPort {
labels."00-name" = mkLabel "*" 1 {};
};
};
}
];
idForInterface = node: interfaceId: "children.node:${node.id}.ports.interface:${interfaceId}";
nodeInterfaceToElk = node: interface:
nodeInterfaceToElk = node: interface: let
interfaceLabels =
{
"00-name" = mkLabel interface.id 1 {};
}
// optionalAttrs (interface.mac != null) {
"50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";};
}
// optionalAttrs (interface.addresses != []) {
"60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";};
};
in
[
# Interface for node in main view
{
children."node:${node.id}".ports."interface:${interface.id}" = {
properties = optionalAttrs (node.preferredRenderType == "card") {
children."node:${node.id}".ports."interface:${interface.id}" = mkPort {
properties = optionalAttrs (node.renderer.preferredType == "card") {
"port.side" = "WEST";
};
width = 8;
height = 8;
style.stroke = "#485263"; # FIXME: TODO color based on attached network color (autoshade)?
style.fill = "#b6beca";
labels =
{
"00-name" = mkLabel interface.id 1 {};
}
// optionalAttrs (interface.mac != null) {
"50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";};
}
// optionalAttrs (interface.addresses != []) {
"60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";};
};
labels = interfaceLabels;
};
}
# Interface for node in network-centric view
{
children.network.children."node:${node.id}".ports."interface:${interface.id}" = {
# FIXME: TODO: deduplicate, same as above
# FIXME: TODO: deduplicate, same as above
# FIXME: TODO: deduplicate, same as above
width = 8;
height = 8;
style.stroke = "#485263"; # FIXME: TODO color based on attached network color (autoshade)?
style.fill = "#b6beca";
labels =
{
"00-name" = mkLabel interface.id 1 {};
}
// optionalAttrs (interface.mac != null) {
"50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";};
}
// optionalAttrs (interface.addresses != []) {
"60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";};
};
children.network.children."node:${node.id}".ports."interface:${interface.id}" = mkPort {
labels = interfaceLabels;
};
}
# Edge in network-centric view
(optionalAttrs (interface.network != null) (
mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}" {
mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" {
style.stroke = config.networks.${interface.network}.color;
}
))
]
++ optionals (!interface.virtual) (flip map interface.physicalConnections (
conn:
++ flatten (flip map interface.physicalConnections (
conn: let
otherInterface = config.nodes.${conn.node}.interfaces.${conn.interface};
in
optionalAttrs (
(!any (y: y.node == node.id && y.interface == interface.id) config.nodes.${conn.node}.interfaces.${conn.interface}.physicalConnections)
(!any (y: y.node == node.id && y.interface == interface.id) otherInterface.physicalConnections)
|| (node.id < conn.node)
) (
# Edge in main view
mkEdge
(idForInterface node interface.id)
(idForInterface config.nodes.${conn.node} conn.interface)
{
# FIXME: in interface definition ensure that the two ends of physical connections don't have different networks (output warning only)
style = optionalAttrs (interface.network != null) {
stroke = config.networks.${interface.network}.color;
};
}
optional (!interface.renderer.hidePhysicalConnections && !otherInterface.renderer.hidePhysicalConnections) (
# Edge in main view
mkEdge
(idForInterface node interface.id)
(idForInterface config.nodes.${conn.node} conn.interface)
{
style = optionalAttrs (interface.network != null) {
stroke = config.networks.${interface.network}.color;
};
}
)
)
));
@ -175,7 +171,7 @@
{
"portLabels.placement" = "OUTSIDE";
}
// optionalAttrs (node.preferredRenderType == "card") {
// optionalAttrs (node.renderer.preferredType == "card") {
"portConstraints" = "FIXED_SIDE";
};
};
@ -193,10 +189,8 @@
]
++ optional (node.parent != null) (
{
children."node:${node.parent}".ports.guests = {
children."node:${node.parent}".ports.guests = mkPort {
properties."port.side" = "EAST";
width = 8;
height = 8;
style.stroke = "#49d18d";
style.fill = "#78dba9";
labels."00-name" = mkLabel "guests" 1 {};

View file

@ -1,27 +1,20 @@
# TODO:
# - stack interfaces horizontally, information is now on the ports anyway.
# - systemd extractor remove cidr mask
# - address port label render make newline capable (multiple port labels)
# - mac address show!
# - split network layout or make rectpacking of childs
# - NAT indication
# - bottom hw image distorted in card view (move to top anyway)
# - embed font globally, try removing satori embed?
# - network overview card (list all networks with name and cidr, legend style)
# - colors!
# - ip labels on edges
# - network centric view
# - better layout for interfaces in svg
# - sevice infos
# - disks (from disko) + render
# - stable pseudorandom colors from palette with no-reuse until necessary
# - network centric view as standalone
# - split network layout or make rectpacking of childs
# - hardware info (image small top and image big bottom and full (no card), maybe just image and render position)
# - more service info
# - disks (from disko) + render
# - impermanence render?
# - nixos nftables firewall render?
# - stable pseudorandom colors from palette with no-reuse until necessary
# - search todo and do
# - podman / docker harvesting
# - systemd extractor remove cidr mask
# - nixos-container extractor
# - search todo and do
{
config,
lib,
@ -144,24 +137,16 @@
};
node = rec {
mkInterface = interface: let
color =
if interface.virtual
then "#7a899f"
else "#70a5eb";
in
/*
html
*/
''
<div tw="flex flex-row items-center my-2">
<div tw="flex flex-row flex-none items-center bg-[${color}] text-[#101419] rounded-lg px-2 py-1 w-46 h-8 mx-4">
${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)}
<span tw="font-bold">${interface.id}</span>
</div>
<span>${toString interface.addresses}</span>
</div>
'';
mkInterface = interface:
/*
html
*/
''
<div tw="flex flex-col flex-none items-center border-[#21262e] border-2 rounded-lg px-2 py-1 m-1">
${mkImage "w-8 h-8 m-1" (config.lib.icons.get interface.icon)}
<span tw="font-bold text-xs">${interface.id}</span>
</div>
'';
serviceDetail = detail:
/*
@ -217,19 +202,6 @@
</div>
'';
mkTitle = node:
/*
html
*/
''
<div tw="flex flex-row mx-6 mt-2 items-center">
${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)}
<h2 tw="text-2xl font-bold">${node.name}</h2>
<div tw="flex grow min-w-8"></div>
${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get node.deviceIcon)}
</div>
'';
mkCard = node: let
services = filter (x: !x.hidden) (attrValues node.services);
guests = filter (x: x.parent == node.id) (attrValues config.nodes);
@ -241,18 +213,25 @@
html
*/
''
${mkTitle node}
<div tw="flex flex-row mx-6 mt-2 items-center">
${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)}
<div tw="flex flex-col min-h-18 justify-center">
<span tw="text-2xl font-bold">${node.name}</span>
${optionalString (node.hardware.info != null) ''<span tw="text-xs">${node.hardware.info}</span>''}
</div>
<div tw="flex grow min-w-8"></div>
${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get node.deviceIcon)}
</div>
${optionalString (node.interfaces != {}) ''<div tw="flex flex-row flex-wrap items-center my-2 mx-3">''}
${concatLines (map mkInterface (attrValues node.interfaces))}
${optionalString (node.interfaces != {}) spacingMt2}
${optionalString (node.interfaces != {}) ''</div>''}
${concatLines (map mkGuest guests)}
${optionalString (guests != []) spacingMt2}
${concatLines (map (mkService {}) services)}
${optionalString (services != []) spacingMt2}
${mkImageMaybe "w-full h-24" node.hardware.image}
'';
};
@ -267,21 +246,26 @@
''
<div tw="flex flex-row mx-6 mt-2 items-center">
${mkImageMaybe "w-8 h-8" (config.lib.icons.get node.icon)}
<h2 tw="text-2xl font-bold">${node.name}</h2>
${optionalString (node.hardware.image != null -> deviceIconImage != node.hardware.image)
<div tw="flex flex-col min-h-18 justify-center">
<span tw="text-2xl font-bold">${node.name}</span>
${optionalString (node.hardware.info != null) ''<span tw="text-xs">${node.hardware.info}</span>''}
</div>
${optionalString (deviceIconImage != null && node.hardware.image != null -> deviceIconImage != node.hardware.image)
''
<div tw="flex grow min-w-4"></div>
${mkImageMaybe "w-12 h-12" deviceIconImage}
''}
</div>
${mkImageMaybe "h-24" node.hardware.image}
<div tw="flex flex-row w-full justify-center">
${mkImageMaybe "h-24" node.hardware.image}
</div>
'';
};
mkPreferredRender = node:
(
if node.preferredRenderType == "image" && node.hardware.image != null
if node.renderer.preferredType == "image" && node.hardware.image != null
then mkImageWithName
else mkCard
)