feat(topology): add automatic network coloring

This commit is contained in:
oddlama 2024-03-31 23:43:44 +02:00
parent dc4d82c828
commit c3dcc619af
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
6 changed files with 217 additions and 65 deletions

View file

@ -223,12 +223,12 @@
networks.home-lan = { networks.home-lan = {
name = "Home LAN"; name = "Home LAN";
cidrv4 = "192.168.1.0/24"; cidrv4 = "192.168.1.0/24";
color = "#78dba9"; #color = "#78dba9";
}; };
networks.home-fritzbox = { networks.home-fritzbox = {
name = "Home Fritzbox"; name = "Home Fritzbox";
cidrv4 = "192.168.178.0/24"; cidrv4 = "192.168.178.0/24";
color = "#f1cf8a"; #color = "#f1cf8a";
}; };
nodes.ward.interfaces.lan.network = "home-lan"; nodes.ward.interfaces.lan.network = "home-lan";

View file

@ -6,8 +6,6 @@ f: {
}: let }: let
inherit inherit
(lib) (lib)
all
assertMsg
attrNames attrNames
attrValues attrValues
concatLines concatLines
@ -18,51 +16,26 @@ f: {
flatten flatten
flip flip
foldl' foldl'
isAttrs
length length
mapAttrsToList mapAttrsToList
mkDefault mkDefault
mkIf mkIf
mkOption mkOption
mkOptionType
optional optional
optionalAttrs optionalAttrs
recursiveUpdate recursiveUpdate
reverseList reverseList
showOption
types types
unique unique
warnIf warnIf
; ;
# Checks whether the value is a lazy value without causing inherit
# it's value to be evaluated (import ../topology/lazy.nix lib)
isLazyValue = x: isAttrs x && x ? _lazyValue; isLazyValue
# Constructs a lazy value holding the given value. lazyOf
lazyValue = value: {_lazyValue = value;}; lazyValue
;
# 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; allNodes = attrNames config.nodes;
allInterfacesOf = node: attrNames config.nodes.${node}.interfaces; allInterfacesOf = node: attrNames config.nodes.${node}.interfaces;
@ -323,6 +296,12 @@ in
description = "The other node's interface id."; description = "The other node's interface id.";
type = types.str; type = types.str;
}; };
renderer.reverse = mkOption {
description = "Whether to reverse the edge. Can be useful to affect node positioning if the layouter is directional.";
type = types.bool;
default = false;
};
}; };
}); });
}; };

View file

@ -1,16 +1,101 @@
f: { f: {
lib, lib,
config, config,
options,
... ...
}: let }: let
inherit inherit
(lib) (lib)
attrNames
attrValues attrValues
elemAt
flatten flatten
flip flip
foldl'
imap0
length
mapAttrsToList
mkOption mkOption
mod
optional
optionalAttrs
recursiveUpdate
subtractLists
types types
unique
warnIf
; ;
inherit
(import ../topology/lazy.nix lib)
isLazyValue
lazyOf
lazyValue
;
style = primaryColor: secondaryColor: pattern: {
inherit primaryColor secondaryColor pattern;
};
predefinedStyles = [
(style "#f1cf8a" null "solid")
(style "#70a5eb" null "solid")
(style "#9dd68d" null "solid")
(style "#5fe1ff" null "solid")
(style "#e05f65" null "solid")
(style "#f9a872" null "solid")
(style "#78dba9" null "solid")
(style "#9378de" null "solid")
(style "#c68aee" null "solid")
(style "#f5a6b8" null "solid")
(style "#70a5eb" null "dashed")
(style "#9dd68d" null "dashed")
(style "#f1cf8a" null "dashed")
(style "#5fe1ff" null "dashed")
(style "#e05f65" null "dashed")
(style "#f9a872" null "dashed")
(style "#78dba9" null "dashed")
(style "#9378de" null "dashed")
(style "#c68aee" null "dashed")
(style "#f5a6b8" null "dashed")
(style "#e05f65" "#e3e6eb" "dashed")
(style "#70a5eb" "#e3e6eb" "dashed")
(style "#9378de" "#e3e6eb" "dashed")
(style "#9dd68d" "#707379" "dashed")
(style "#f1cf8a" "#707379" "dashed")
(style "#5fe1ff" "#707379" "dashed")
(style "#f9a872" "#707379" "dashed")
(style "#78dba9" "#707379" "dashed")
(style "#c68aee" "#707379" "dashed")
(style "#f5a6b8" "#707379" "dashed")
];
# A map containing all networks that have an explicitly assigned style, and the style.
explicitStyles = foldl' recursiveUpdate {} (flatten (
flip map options.networks.definitions (mapAttrsToList (
netId: net:
optional (net ? style && !isLazyValue net.style) {
${netId} = net.style;
}
))
));
# All unused predefined styles
remainingStyles = subtractLists (unique (attrValues explicitStyles)) predefinedStyles;
# All networks without styles
remainingNets = subtractLists (attrNames explicitStyles) (attrNames config.networks);
# Fold over all networks that have no style and assign the next free one. Warn and repeat from beginning if necessary.
computedStyles =
explicitStyles
// warnIf
(length remainingNets > length remainingStyles)
"topology: There are more networks without styles than predefined styles. Some styles will have to be reused!"
(
foldl' recursiveUpdate {} (imap0 (i: net: {
${net} = elemAt remainingStyles (mod i (length remainingStyles));
})
remainingNets)
);
in in
f { f {
options.networks = mkOption { options.networks = mkOption {
@ -37,15 +122,20 @@ in
default = null; default = null;
}; };
color = mkOption { style =
description = "The color of this network"; mkOption {
default = "random"; description = ''
apply = x: A style for this network, usually used to draw connections.
if x == "random" Must be an attrset consisting of three attributes:
then "#ff00ff" # FIXME: TODO: lookuptable linear probing hashing into palette - primaryColor (#rrggbb): The primary color, usually the color of edges.
else x; - secondaryColor (#rrggbb): The secondary color, usually the background of a dashed line and only shown when pattern != solid. Set to null for transparent.
type = types.either (types.strMatching "^#[0-9a-f]{6}$") (types.enum ["random"]); - pattern (solid, dashed, dotted): The pattern to use.
}; '';
type = lazyOf types.attrs;
}
// optionalAttrs config.topology.isMainModule {
default = lazyValue computedStyles.${networkSubmod.config.id};
};
cidrv4 = mkOption { cidrv4 = mkOption {
description = "The CIDRv4 address space of this network or null if it doesn't use ipv4"; description = "The CIDRv4 address space of this network or null if it doesn't use ipv4";

View file

@ -0,0 +1,42 @@
lib: let
inherit
(lib)
all
assertMsg
isAttrs
mkOptionType
showOption
types
;
# 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;
in {
inherit isLazyValue lazyValue lazyValueOf lazyOf;
}

View file

@ -54,14 +54,17 @@
else value else value
); );
mkEdge = from: to: extra: { mkEdge = from: to: reverse: extra:
edges."${from}__${to}" = if reverse
{ then mkEdge to from false extra
sources = [from]; else {
targets = [to]; edges."${from}__${to}" =
} {
// extra; sources = [from];
}; targets = [to];
}
// extra;
};
mkPort = recursiveUpdate { mkPort = recursiveUpdate {
width = 8; width = 8;
@ -81,6 +84,32 @@
// extraStyle; // extraStyle;
}; };
pathStyleFromNetworkStyle = style:
{
solid = {
stroke = style.primaryColor;
};
dashed =
{
stroke = style.primaryColor;
stroke-dasharray = "10,8";
stroke-linecap = "round";
}
// optionalAttrs (style.secondaryColor != null) {
background = style.secondaryColor;
};
dotted =
{
stroke = style.primaryColor;
stroke-dasharray = "2,6";
stroke-linecap = "round";
}
// optionalAttrs (style.secondaryColor != null) {
background = style.secondaryColor;
};
}
.${style.pattern};
netToElk = net: [ netToElk = net: [
{ {
children.network.children."net:${net.id}" = { children.network.children."net:${net.id}" = {
@ -100,15 +129,18 @@
idForInterface = node: interfaceId: "children.node:${node.id}.ports.interface:${interfaceId}"; idForInterface = node: interfaceId: "children.node:${node.id}.ports.interface:${interfaceId}";
nodeInterfaceToElk = node: interface: let nodeInterfaceToElk = node: interface: let
netStyle = optionalAttrs (interface.network != null) {
fill = config.networks.${interface.network}.style.primaryColor;
};
interfaceLabels = interfaceLabels =
{ {
"00-name" = mkLabel interface.id 1 {}; "00-name" = mkLabel interface.id 1 {};
} }
// optionalAttrs (interface.mac != null) { // optionalAttrs (interface.mac != null) {
"50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";}; "50-mac" = mkLabel interface.mac 1 netStyle;
} }
// optionalAttrs (interface.addresses != []) { // optionalAttrs (interface.addresses != []) {
"60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";}; "60-addrs" = mkLabel (toString interface.addresses) 1 netStyle;
}; };
in in
[ [
@ -131,8 +163,8 @@
# Edge in network-centric view # Edge in network-centric view
(optionalAttrs (interface.network != null) ( (optionalAttrs (interface.network != null) (
mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" { mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" false {
style.stroke = config.networks.${interface.network}.color; style = pathStyleFromNetworkStyle config.networks.${interface.network}.style;
} }
)) ))
] ]
@ -149,10 +181,11 @@
mkEdge mkEdge
(idForInterface node interface.id) (idForInterface node interface.id)
(idForInterface config.nodes.${conn.node} conn.interface) (idForInterface config.nodes.${conn.node} conn.interface)
conn.renderer.reverse
{ {
style = optionalAttrs (interface.network != null) { style = optionalAttrs (interface.network != null) (
stroke = config.networks.${interface.network}.color; pathStyleFromNetworkStyle config.networks.${interface.network}.style
}; );
} }
) )
) )
@ -196,7 +229,7 @@
labels."00-name" = mkLabel "guests" 1 {}; labels."00-name" = mkLabel "guests" 1 {};
}; };
} }
// mkEdge "children.node:${node.parent}.ports.guests" "children.node:${node.id}" { // mkEdge "children.node:${node.parent}.ports.guests" "children.node:${node.id}" false {
style.stroke-dasharray = "10,8"; style.stroke-dasharray = "10,8";
style.stroke-linecap = "round"; style.stroke-linecap = "round";
} }

View file

@ -113,10 +113,18 @@
mkCard = net: { mkCard = net: {
width = 480; width = 480;
html = let html = let
netColor = netStylePreview = let
if net.color != null secondaryColor =
then net.color if net.style.secondaryColor == null
else "#b6beca"; then "#00000000"
else net.style.secondaryColor;
in
{
solid = ''<div tw="flex flex-none bg-[${net.style.primaryColor}] w-8 h-4 mr-4 rounded-md"></div>'';
dashed = ''<div tw="flex flex-none w-8 h-4 mr-4 rounded-md" style="backgroundImage: linear-gradient(90deg, ${net.style.primaryColor} 0%, ${net.style.primaryColor} 50%, ${secondaryColor} 50.01%, ${secondaryColor} 100%);"></div>'';
dotted = ''<div tw="flex flex-none w-8 h-4 mr-4 rounded-md" style="backgroundImage: radial-gradient(circle, ${net.style.primaryColor} 30%, ${secondaryColor} 30.01%);"></div>'';
}
.${net.style.pattern};
in in
mkCardContainer mkCardContainer
/* /*
@ -124,7 +132,7 @@
*/ */
'' ''
<div tw="flex flex-row mx-6 mt-2 items-center"> <div tw="flex flex-row mx-6 mt-2 items-center">
<div tw="flex flex-none bg-[${netColor}] w-8 h-8 mr-4 rounded-lg"></div> ${netStylePreview}
<h2 tw="text-2xl font-bold">${net.name}</h2> <h2 tw="text-2xl font-bold">${net.name}</h2>
<div tw="flex grow min-w-8"></div> <div tw="flex grow min-w-8"></div>
${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get net.icon)} ${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get net.icon)}