From 89f0646f74393f6374a24310507bca8eabe61465 Mon Sep 17 00:00:00 2001
From: Ghost User <ghost@example.com>
Date: Thu, 2 Jan 2025 08:34:09 +0000
Subject: [PATCH 01/45] [renovate] Update module golang.org/x/net to v0.32.0

See merge request danet/gosdn!1125

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 4 ++--
 go.sum | 4 ++++
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 2879fe711..15aaf3ba0 100644
--- a/go.mod
+++ b/go.mod
@@ -77,8 +77,8 @@ require (
 	github.com/xdg-go/stringprep v1.0.4 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
-	golang.org/x/crypto v0.30.0
-	golang.org/x/net v0.31.0
+	golang.org/x/crypto v0.31.0
+	golang.org/x/net v0.33.0
 	golang.org/x/sys v0.28.0 // indirect
 	golang.org/x/term v0.27.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
diff --git a/go.sum b/go.sum
index 880acc9d1..192ef1d3f 100644
--- a/go.sum
+++ b/go.sum
@@ -411,6 +411,8 @@ golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
 golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
 golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
 golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
@@ -446,6 +448,8 @@ golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
 golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
 golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-- 
GitLab


From 078238f236244c68183e92b03d71ae976ac9f463 Mon Sep 17 00:00:00 2001
From: Ghost User <ghost@example.com>
Date: Thu, 2 Jan 2025 08:43:17 +0000
Subject: [PATCH 02/45] [renovate] Update renovate/renovate Docker tag to
 v39.60.0

See merge request danet/gosdn!1127

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.renovate.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.renovate.yml b/.gitlab/ci/.renovate.yml
index 55c8ea335..5faf72ffd 100644
--- a/.gitlab/ci/.renovate.yml
+++ b/.gitlab/ci/.renovate.yml
@@ -1,7 +1,7 @@
 renovate:
     stage: tools
 
-    image: renovate/renovate:39.48.1
+    image: renovate/renovate:39.87.0
 
     variables:
         LOG_LEVEL: debug
-- 
GitLab


From a1fd8bda7ff49de285e42ae0e0c575e54e5b2161 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Thu, 2 Jan 2025 08:56:41 +0000
Subject: [PATCH 03/45] [renovate] Update module google.golang.org/grpc to
 v1.69.2

See merge request danet/gosdn!1132

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 15aaf3ba0..6aeeb8613 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@ require (
 	github.com/stretchr/testify v1.10.0
 	go.mongodb.org/mongo-driver v1.17.1
 	golang.org/x/sync v0.10.0
-	google.golang.org/grpc v1.68.1
+	google.golang.org/grpc v1.69.2
 	google.golang.org/protobuf v1.35.2
 	gopkg.in/yaml.v3 v3.0.1
 )
diff --git a/go.sum b/go.sum
index 192ef1d3f..187503b7b 100644
--- a/go.sum
+++ b/go.sum
@@ -617,6 +617,8 @@ google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
 google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
 google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
 google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
+google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
+google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-- 
GitLab


From c97021c2269817af40b0a8b7cd60aac952a8b202 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Thu, 2 Jan 2025 09:07:42 +0000
Subject: [PATCH 04/45] [renovate] Update module github.com/openconfig/gnmi to
 v0.12.0

See merge request danet/gosdn!1133

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 4 ++--
 go.sum | 4 ++++
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 6aeeb8613..d0adaae95 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0
 	github.com/mitchellh/go-homedir v1.1.0
-	github.com/openconfig/gnmi v0.11.0
+	github.com/openconfig/gnmi v0.12.0
 	github.com/openconfig/goyang v1.6.0
 	github.com/openconfig/ygot v0.29.20
 	github.com/prometheus/client_golang v1.20.5
@@ -122,7 +122,7 @@ require (
 	github.com/stoewer/go-strcase v1.3.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
-	golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect
+	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
 	gotest.tools/v3 v3.5.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 187503b7b..6b8f0e621 100644
--- a/go.sum
+++ b/go.sum
@@ -262,6 +262,8 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV
 github.com/openconfig/gnmi v0.10.0/go.mod h1:Y9os75GmSkhHw2wX8sMsxfI7qRGAEcDh8NTa5a8vj6E=
 github.com/openconfig/gnmi v0.11.0 h1:H7pLIb/o3xObu3+x0Fv9DCK7TH3FUh7mNwbYe+34hFw=
 github.com/openconfig/gnmi v0.11.0/go.mod h1:9oJSQPPCpNvfMRj8e4ZoLVAw4wL8HyxXbiDlyuexCGU=
+github.com/openconfig/gnmi v0.12.0 h1:aPkmcX9pdcz6QqsBsXXg5UQooqhnmlHD3JtdtvtzmaU=
+github.com/openconfig/gnmi v0.12.0/go.mod h1:5a/cIOZevJLfJgd1qWkgYROE8xfgEbaSJXpdD8xk/LQ=
 github.com/openconfig/goyang v0.0.0-20200115183954-d0a48929f0ea/go.mod h1:dhXaV0JgHJzdrHi2l+w0fZrwArtXL7jEFoiqLEdmkvU=
 github.com/openconfig/goyang v1.6.0 h1:JjnPbLY1/y28VyTO67LsEV0TaLWNiZyDcsppGq4F4is=
 github.com/openconfig/goyang v1.6.0/go.mod h1:sdNZi/wdTZyLNBNfgLzmmbi7kISm7FskMDKKzMY+x1M=
@@ -416,6 +418,8 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
+golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-- 
GitLab


From 2b36e705948a4af7477509f438ebec55ee1b847d Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Thu, 2 Jan 2025 09:22:54 +0000
Subject: [PATCH 05/45] [renovate] Update golangci/golangci-lint Docker tag to
 v1.63.1

See merge request danet/gosdn!1137

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.code-quality-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.code-quality-ci.yml b/.gitlab/ci/.code-quality-ci.yml
index d054508c4..56ee84f2e 100644
--- a/.gitlab/ci/.code-quality-ci.yml
+++ b/.gitlab/ci/.code-quality-ci.yml
@@ -1,5 +1,5 @@
 code-quality:
-    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.62.2-alpine
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.63.1-alpine
     stage: analyze
     script:
         # writes golangci-lint output to gl-code-quality-report.json
-- 
GitLab


From 479b5c6811bcd54739da94c475531b7d2f95644d Mon Sep 17 00:00:00 2001
From: Ghost User <ghost@example.com>
Date: Fri, 3 Jan 2025 07:48:03 +0000
Subject: [PATCH 06/45] [renovate] Update
 google.golang.org/genproto/googleapis/api digest to e6fa225

See merge request danet/gosdn!1126

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 6 +++---
 go.sum | 6 ++++++
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index d0adaae95..5e246227c 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,7 @@ require (
 	go.mongodb.org/mongo-driver v1.17.1
 	golang.org/x/sync v0.10.0
 	google.golang.org/grpc v1.69.2
-	google.golang.org/protobuf v1.35.2
+	google.golang.org/protobuf v1.36.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -91,7 +91,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.4.10
 	github.com/lesismal/nbio v1.5.12
-	google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a
+	google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d
 )
 
 require (
@@ -123,6 +123,6 @@ require (
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
 	gotest.tools/v3 v3.5.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 6b8f0e621..f71564f8d 100644
--- a/go.sum
+++ b/go.sum
@@ -583,6 +583,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
 google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
 google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
+google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
@@ -603,6 +605,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -643,6 +647,8 @@ google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFyt
 google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
 google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
+google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-- 
GitLab


From a0f745dacb2690efb194b4b25ce5d51bd3c71be2 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Fri, 3 Jan 2025 07:56:49 +0000
Subject: [PATCH 07/45] [renovate] Update module
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go to
 v1.36.1-20241127180247-a33202765966.1

See merge request danet/gosdn!1134

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 5e246227c..9125b3bc1 100644
--- a/go.mod
+++ b/go.mod
@@ -86,7 +86,7 @@ require (
 )
 
 require (
-	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1
+	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1
 	github.com/bufbuild/protovalidate-go v0.7.3
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.4.10
diff --git a/go.sum b/go.sum
index f71564f8d..3344b045b 100644
--- a/go.sum
+++ b/go.sum
@@ -16,6 +16,8 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-2024092016423
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20240920164238-5a7b106cbb87.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1 h1:jLd96rDDNJ+zIJxvV/L855VEtrjR0G4aePVDlCpf6kw=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1 h1:v223wh/bhlSHSc0tU9PXRWXHhkw3UWMtth7TmYGfHAQ=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1/go.mod h1:/zlFuuECgFgewxwW6qQKgvMJ07YZkWlVkcSxEhJprJw=
 cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
 cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-- 
GitLab


From face532ae93d3d850dc6f7b65ad69fc450334cf9 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Fri, 3 Jan 2025 08:08:45 +0000
Subject: [PATCH 08/45] [renovate] Update golangci/golangci-lint Docker tag to
 v1.63.3

See merge request danet/gosdn!1138

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.code-quality-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.code-quality-ci.yml b/.gitlab/ci/.code-quality-ci.yml
index 56ee84f2e..1273f2e07 100644
--- a/.gitlab/ci/.code-quality-ci.yml
+++ b/.gitlab/ci/.code-quality-ci.yml
@@ -1,5 +1,5 @@
 code-quality:
-    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.63.1-alpine
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.63.3-alpine
     stage: analyze
     script:
         # writes golangci-lint output to gl-code-quality-report.json
-- 
GitLab


From 3215297beae2b1b5ce1db347de6ce9cfcec288ce Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Fri, 3 Jan 2025 08:20:19 +0000
Subject: [PATCH 09/45] [renovate] Update renovate/renovate Docker tag to
 v39.90.1

See merge request danet/gosdn!1139

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.renovate.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.renovate.yml b/.gitlab/ci/.renovate.yml
index 5faf72ffd..fe2b9ce8c 100644
--- a/.gitlab/ci/.renovate.yml
+++ b/.gitlab/ci/.renovate.yml
@@ -1,7 +1,7 @@
 renovate:
     stage: tools
 
-    image: renovate/renovate:39.87.0
+    image: renovate/renovate:39.90.1
 
     variables:
         LOG_LEVEL: debug
-- 
GitLab


From 5d7d372fb64d72ea69d672564ca2b1c95e62b853 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Mon, 6 Jan 2025 07:48:48 +0000
Subject: [PATCH 10/45] [renovate] Update module
 github.com/grpc-ecosystem/grpc-gateway/v2 to v2.25.1

See merge request danet/gosdn!1136

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 9125b3bc1..21df761c9 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/docker/docker v24.0.9+incompatible
 	github.com/google/go-cmp v0.6.0
 	github.com/google/uuid v1.6.0
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/openconfig/gnmi v0.12.0
 	github.com/openconfig/goyang v1.6.0
diff --git a/go.sum b/go.sum
index 3344b045b..0ba985a52 100644
--- a/go.sum
+++ b/go.sum
@@ -171,6 +171,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-- 
GitLab


From 9b886a0e9d538c6d118902ecb4832e280d6e6f4b Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Mon, 6 Jan 2025 08:00:03 +0000
Subject: [PATCH 11/45] [renovate] Update golangci/golangci-lint Docker tag to
 v1.63.4

See merge request danet/gosdn!1141

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.code-quality-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.code-quality-ci.yml b/.gitlab/ci/.code-quality-ci.yml
index 1273f2e07..95ed3abe9 100644
--- a/.gitlab/ci/.code-quality-ci.yml
+++ b/.gitlab/ci/.code-quality-ci.yml
@@ -1,5 +1,5 @@
 code-quality:
-    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.63.3-alpine
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.63.4-alpine
     stage: analyze
     script:
         # writes golangci-lint output to gl-code-quality-report.json
-- 
GitLab


From 85456e24c180a7b34a20abdd83f1fd028f6f245f Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Mon, 6 Jan 2025 08:20:28 +0000
Subject: [PATCH 12/45] [renovate] Update renovate/renovate Docker tag to
 v39.91.0

See merge request danet/gosdn!1142

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.renovate.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.renovate.yml b/.gitlab/ci/.renovate.yml
index fe2b9ce8c..57dfa95c0 100644
--- a/.gitlab/ci/.renovate.yml
+++ b/.gitlab/ci/.renovate.yml
@@ -1,7 +1,7 @@
 renovate:
     stage: tools
 
-    image: renovate/renovate:39.90.1
+    image: renovate/renovate:39.91.0
 
     variables:
         LOG_LEVEL: debug
-- 
GitLab


From 45dcefa1e6a3521d5244b86f6698ed56cf13fc4f Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Tue, 7 Jan 2025 07:43:41 +0000
Subject: [PATCH 13/45] [renovate] Update
 google.golang.org/genproto/googleapis/api digest to 5f5ef82

See merge request danet/gosdn!1143

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 4 ++--
 go.sum | 4 ++++
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 21df761c9..6e4cf99a4 100644
--- a/go.mod
+++ b/go.mod
@@ -91,7 +91,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.4.10
 	github.com/lesismal/nbio v1.5.12
-	google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d
+	google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422
 )
 
 require (
@@ -123,6 +123,6 @@ require (
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
 	gotest.tools/v3 v3.5.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 0ba985a52..e7d3f5158 100644
--- a/go.sum
+++ b/go.sum
@@ -589,6 +589,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
 google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
 google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
@@ -611,6 +613,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-- 
GitLab


From 11a1f3f00d3b4de9213b33c855189b63fcf480e8 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Tue, 7 Jan 2025 07:55:01 +0000
Subject: [PATCH 14/45] [renovate] Update renovate/renovate Docker tag to
 v39.91.3

See merge request danet/gosdn!1144

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.renovate.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.renovate.yml b/.gitlab/ci/.renovate.yml
index 57dfa95c0..03f8fddfa 100644
--- a/.gitlab/ci/.renovate.yml
+++ b/.gitlab/ci/.renovate.yml
@@ -1,7 +1,7 @@
 renovate:
     stage: tools
 
-    image: renovate/renovate:39.91.0
+    image: renovate/renovate:39.91.3
 
     variables:
         LOG_LEVEL: debug
-- 
GitLab


From 04893c56040c397ba3004ae21c5fb74ae60aef3c Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Tue, 7 Jan 2025 11:37:12 +0100
Subject: [PATCH 15/45] add react ui to containerlab

---
 .gitlab/ci/.build-binaries.yml                    |  1 +
 .gitlab/ci/.build-container-images.yml            | 10 ++++++++++
 .gitlab/ci/.react-ui.yml                          |  3 ---
 Makefile                                          |  2 +-
 dev_env_data/clab/gosdn_slim.clab.yaml            |  7 +++++++
 .../docker-compose/basic_docker-compose.yml       |  5 +++++
 makefiles/container/Makefile                      |  3 +++
 plugin-registry/plugin-registry.Dockerfile        |  7 ++++---
 react-ui/.dockerignore                            |  5 +++++
 react-ui/docker/webserver/Dockerfile              | 15 ++++++++++++---
 react-ui/docker/webserver/nginx.conf              |  1 -
 react-ui/scripts/build.sh                         |  2 +-
 react-ui/vite.config.mjs                          |  3 ++-
 13 files changed, 51 insertions(+), 13 deletions(-)
 delete mode 100644 .gitlab/ci/.react-ui.yml
 create mode 100644 react-ui/.dockerignore

diff --git a/.gitlab/ci/.build-binaries.yml b/.gitlab/ci/.build-binaries.yml
index 174203fdc..4877cd169 100644
--- a/.gitlab/ci/.build-binaries.yml
+++ b/.gitlab/ci/.build-binaries.yml
@@ -16,5 +16,6 @@ build-all-binaries:
           - artifacts/venv-manager
           - artifacts/inventory-manager
           - artifacts/plugin-registry
+          - artifacts/react-ui
         expire_in: 1 week
     <<: *build-binaries
diff --git a/.gitlab/ci/.build-container-images.yml b/.gitlab/ci/.build-container-images.yml
index da70dcba2..79e22bc63 100644
--- a/.gitlab/ci/.build-container-images.yml
+++ b/.gitlab/ci/.build-container-images.yml
@@ -75,6 +75,16 @@ build-inventory-manager-image:
         - docker push "$INVENTORY_MANAGER_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
     <<: *build
 
+build-react-ui-image:
+    script:
+        - REACT_UI_IMAGE_NAME="${CI_REGISTRY_IMAGE}/react-ui"
+        - docker buildx build -t "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA" -f "${CI_PROJECT_DIR}/react-ui/docker/webserver/Dockerfile" ./react-ui
+        - docker push "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA"
+        - docker tag "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA" "$REACT_UI_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
+        - docker push "$REACT_UI_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
+    <<: *build
+    
+
 build-integration-test-images:
     needs: ["build-controller-image"]
     script:
diff --git a/.gitlab/ci/.react-ui.yml b/.gitlab/ci/.react-ui.yml
deleted file mode 100644
index 873b694c8..000000000
--- a/.gitlab/ci/.react-ui.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-build-react-ui:
-  stage: build
-  
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 888644abe..73ae77b6a 100644
--- a/Makefile
+++ b/Makefile
@@ -60,7 +60,7 @@ lint-fix: install-tools
 
 build: pre build-gosdn build-gosdnc build-plugin-registry build-venv-manager build-arista-routing-engine-app build-hostname-checker-app build-basic-interface-monitoring-app build-inventory-manager
 
-containerize-all: containerize-gosdn containerize-gosdnc containerize-plugin-registry containerize-venv-manager containerize-arista-routing-engine-app containerize-inventory-manager
+containerize-all: containerize-gosdn containerize-gosdnc containerize-plugin-registry containerize-venv-manager containerize-arista-routing-engine-app containerize-inventory-manager containerize-react-ui
 
 generate-all-certs: pre generate-root-ca generate-gosdn-certs generate-gnmi-target-certs
 
diff --git a/dev_env_data/clab/gosdn_slim.clab.yaml b/dev_env_data/clab/gosdn_slim.clab.yaml
index f4d76846c..b3ef8eb08 100644
--- a/dev_env_data/clab/gosdn_slim.clab.yaml
+++ b/dev_env_data/clab/gosdn_slim.clab.yaml
@@ -13,6 +13,13 @@ topology:
       image: plugin-registry
       mgmt-ipv4: 172.100.0.16
 
+    react-ui:
+      kind: linux
+      image: react-ui
+      ports:
+        - 127.0.0.1:8088:80
+      mgmt-ipv4: 172.100.0.6
+
     gosdn:
       kind: linux
       image: gosdn
diff --git a/dev_env_data/docker-compose/basic_docker-compose.yml b/dev_env_data/docker-compose/basic_docker-compose.yml
index 4e7664161..2544e7b0f 100644
--- a/dev_env_data/docker-compose/basic_docker-compose.yml
+++ b/dev_env_data/docker-compose/basic_docker-compose.yml
@@ -75,5 +75,10 @@ services:
     command:
       start --cert /etc/gnmi-target/ssl/certs/gnmi-target-selfsigned.crt --key /etc/gnmi-target/ssl/private/gnmi-target-selfsigned.key --ca_file /etc/gnmi-target/ssl/ca.crt
 
+  react-ui:
+    image: react-ui
+    ports:
+      - 127.0.0.1:8088:80
+
 volumes:
     mongo-db-basic:
diff --git a/makefiles/container/Makefile b/makefiles/container/Makefile
index c9c21b972..6e3b97242 100644
--- a/makefiles/container/Makefile
+++ b/makefiles/container/Makefile
@@ -25,3 +25,6 @@ containerize-ws-events-app:
 
 containerize-inventory-manager:
 	docker buildx build --rm -t venv-manager --load -f applications/inventory-manager/inventory-manager.Dockerfile .
+
+containerize-react-ui:
+	docker buildx build --rm --load -t react-ui -f react-ui/docker/webserver/Dockerfile .
diff --git a/plugin-registry/plugin-registry.Dockerfile b/plugin-registry/plugin-registry.Dockerfile
index 05d3aad83..292a134fd 100644
--- a/plugin-registry/plugin-registry.Dockerfile
+++ b/plugin-registry/plugin-registry.Dockerfile
@@ -4,8 +4,9 @@ ARG GITLAB_PROXY
 
 FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-bookworm as builder
 WORKDIR /plugin-registry/
-RUN apt-get update
-RUN apt-get -y install --no-install-recommends zip
+RUN apt-get update && \
+    apt-get -y install --no-install-recommends zip && \
+    rm -rf /var/lib/apt/lists/*
 COPY . .
 RUN --mount=type=cache,target=/root/go/pkg/mod \
     --mount=type=cache,target=/root/.cache/go-build \
@@ -21,4 +22,4 @@ WORKDIR /app/
 COPY --from=builder /plugin-registry/artifacts/plugin-registry .
 COPY --from=builder /plugin-registry/plugin-registry/plugins ./plugins
 COPY --from=builder /plugin-registry/dev_env_data/plugin-registry/plugin-store.json ./plugin-store.json
-ENTRYPOINT ["./plugin-registry", "-socket", "55057"]
+ENTRYPOINT ["./plugin-registry", "-socket", "55057"]
\ No newline at end of file
diff --git a/react-ui/.dockerignore b/react-ui/.dockerignore
new file mode 100644
index 000000000..5ecec0472
--- /dev/null
+++ b/react-ui/.dockerignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+vite/
+tmp/
+.vscode/
\ No newline at end of file
diff --git a/react-ui/docker/webserver/Dockerfile b/react-ui/docker/webserver/Dockerfile
index f4644ee10..71127c09b 100644
--- a/react-ui/docker/webserver/Dockerfile
+++ b/react-ui/docker/webserver/Dockerfile
@@ -1,4 +1,13 @@
-FROM nginx:alpine3.20
+FROM node:alpine3.20 as builder
+
+COPY  ./api/openapiv2/gosdn_northbound.swagger.json /app/api/openapiv2/gosdn_northbound.swagger.json
+COPY ./react-ui /app/react-ui
+
+RUN cd /app/react-ui && yarn && yarn build
 
-COPY dist /usr/share/nginx/html
-COPY docker/webserver/nginx.conf /etc/nginx/nginx.conf 
\ No newline at end of file
+
+# webserver
+FROM nginx:alpine3.20
+COPY --from=builder /app/react-ui/dist /usr/share/nginx/html
+COPY --from=builder /app/react-ui/docker/webserver/nginx.conf /etc/nginx/nginx.conf 
+EXPOSE 80
\ No newline at end of file
diff --git a/react-ui/docker/webserver/nginx.conf b/react-ui/docker/webserver/nginx.conf
index 87066f6ab..eb4fc2be9 100644
--- a/react-ui/docker/webserver/nginx.conf
+++ b/react-ui/docker/webserver/nginx.conf
@@ -41,7 +41,6 @@ http {
 
         #access_log  logs/host.access.log  main;
 
-
         location ^~ /api/ {
             proxy_set_header Host $host;
             proxy_set_header X-Real-IP $remote_addr;
diff --git a/react-ui/scripts/build.sh b/react-ui/scripts/build.sh
index ad0f0cd0b..fb6cb2a21 100755
--- a/react-ui/scripts/build.sh
+++ b/react-ui/scripts/build.sh
@@ -7,7 +7,7 @@ docker run --rm \
     -w /app \
     -u $(id -u):$(id -g) \
     $IMAGE \
-    yarn install && yarn build
+    yarn && yarn build
 
 if [ $? -ne 0 ]; then
     echo "Error while building frontend app"
diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs
index fefafe05a..fc4ce4a01 100755
--- a/react-ui/vite.config.mjs
+++ b/react-ui/vite.config.mjs
@@ -8,10 +8,11 @@ export default defineConfig({
     },
     // develop server
     server: {
+        host: true,
         port: 3000,
         proxy: {
             '/api': {
-                target: 'http://127.0.0.1:8080',
+                target: 'http://clab-gosdn_csbi_arista_base-gosdn:8080',
                 changeOrigin: true,
                 secure: false,
                 rewrite: (path) => path.replace(/^\/api/, ''),
-- 
GitLab


From 86786eb2b70bb9fa83084eba02a61c6c32c1dbaf Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Tue, 7 Jan 2025 12:45:35 +0100
Subject: [PATCH 16/45] fix ci docker image build path

---
 .gitlab/ci/.build-container-images.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.build-container-images.yml b/.gitlab/ci/.build-container-images.yml
index 79e22bc63..7fda52178 100644
--- a/.gitlab/ci/.build-container-images.yml
+++ b/.gitlab/ci/.build-container-images.yml
@@ -78,7 +78,7 @@ build-inventory-manager-image:
 build-react-ui-image:
     script:
         - REACT_UI_IMAGE_NAME="${CI_REGISTRY_IMAGE}/react-ui"
-        - docker buildx build -t "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA" -f "${CI_PROJECT_DIR}/react-ui/docker/webserver/Dockerfile" ./react-ui
+        - docker buildx build -t "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA" -f "${CI_PROJECT_DIR}/react-ui/docker/webserver/Dockerfile" .
         - docker push "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA"
         - docker tag "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA" "$REACT_UI_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
         - docker push "$REACT_UI_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
-- 
GitLab


From dc4cdcdaaa87bcae0eaca150d7dff9828382ba02 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Wed, 8 Jan 2025 08:07:42 +0000
Subject: [PATCH 17/45] [renovate] Update module
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go to
 v1.36.2-20241127180247-a33202765966.1

See merge request danet/gosdn!1146

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 4 ++--
 go.sum | 4 ++++
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 6e4cf99a4..1946630f6 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,7 @@ require (
 	go.mongodb.org/mongo-driver v1.17.1
 	golang.org/x/sync v0.10.0
 	google.golang.org/grpc v1.69.2
-	google.golang.org/protobuf v1.36.1
+	google.golang.org/protobuf v1.36.2
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -86,7 +86,7 @@ require (
 )
 
 require (
-	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1
+	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20241127180247-a33202765966.1
 	github.com/bufbuild/protovalidate-go v0.7.3
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.4.10
diff --git a/go.sum b/go.sum
index e7d3f5158..fe429b64a 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-2024112718024
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1 h1:v223wh/bhlSHSc0tU9PXRWXHhkw3UWMtth7TmYGfHAQ=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1/go.mod h1:/zlFuuECgFgewxwW6qQKgvMJ07YZkWlVkcSxEhJprJw=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20241127180247-a33202765966.1 h1:BICM6du/XzvEgeorNo4xgohK3nMTmEPViGyd5t7xVqk=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20241127180247-a33202765966.1/go.mod h1:JnMVLi3qrNYPODVpEKG7UjHLl/d2zR221e66YCSmP2Q=
 cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
 cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@@ -657,6 +659,8 @@ google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8i
 google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
 google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
+google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-- 
GitLab


From 6df3abc19a0357cff45fbe3e3e169ee7c4c5b34f Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Wed, 8 Jan 2025 10:32:16 +0100
Subject: [PATCH 18/45] (ui): add dev env script

---
 react-ui/package.json   |  3 ++-
 react-ui/scripts/dev.sh | 11 +++++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)
 create mode 100755 react-ui/scripts/dev.sh

diff --git a/react-ui/package.json b/react-ui/package.json
index 709c5bfcb..4c9271009 100755
--- a/react-ui/package.json
+++ b/react-ui/package.json
@@ -37,7 +37,8 @@
         "build::api": "npx @rtk-query/codegen-openapi ./scripts/openapi-config.json",
         "build": "yarn build::api && yarn build::frontend",
         "lint": "eslint src",
-        "lint::fix": "eslint src --fix"
+        "lint::fix": "eslint src --fix",
+        "dev": "./scripts/dev.sh"
     },
     "eslintConfig": {
         "extends": [
diff --git a/react-ui/scripts/dev.sh b/react-ui/scripts/dev.sh
new file mode 100755
index 000000000..3a2d20885
--- /dev/null
+++ b/react-ui/scripts/dev.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env sh
+
+docker run \
+    -it \
+    --rm \
+    -v $(pwd):/app \
+    -w /app \
+    -p 127.0.0.1:3000:3000 \
+    --network gosdn-csbi-arista-base-net \
+    node:20-alpine3.20 \
+    npx vite
-- 
GitLab


From f9d974909efc8465433ff37bc7a4cd361a4f711c Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Wed, 8 Jan 2025 11:29:44 +0100
Subject: [PATCH 19/45] wip before changing unsubscribe to category param

---
 .../src/components/devices/api/pnd.fetch.ts   | 14 +++++++
 .../devices/reducer/device.reducer.ts         | 38 +++++++----------
 react-ui/src/index.tsx                        |  1 +
 react-ui/src/shared/api/user.fetch.ts         | 25 +++++++++++
 react-ui/src/shared/helper/debug.ts           |  5 +++
 .../protected.layout/protected.layout.tsx     |  4 +-
 .../src/shared/reducer/routine.reducer.ts     | 42 ++++++++++++++-----
 react-ui/src/shared/reducer/user.reducer.ts   | 30 ++-----------
 react-ui/src/shared/utils/routine.manager.ts  |  2 +-
 9 files changed, 96 insertions(+), 65 deletions(-)
 create mode 100644 react-ui/src/components/devices/api/pnd.fetch.ts
 create mode 100644 react-ui/src/shared/api/user.fetch.ts
 create mode 100644 react-ui/src/shared/helper/debug.ts

diff --git a/react-ui/src/components/devices/api/pnd.fetch.ts b/react-ui/src/components/devices/api/pnd.fetch.ts
new file mode 100644
index 000000000..6e677ab8d
--- /dev/null
+++ b/react-ui/src/components/devices/api/pnd.fetch.ts
@@ -0,0 +1,14 @@
+import { PndServiceGetPndListApiArg, api } from "@api/api"
+import { createAsyncThunk } from "@reduxjs/toolkit"
+import { setPnds } from "../reducer/device.reducer"
+
+export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
+    const payload: PndServiceGetPndListApiArg = {
+        timestamp: new Date().getTime().toString(),
+    }
+
+    const subscription = thunkApi.dispatch(api.endpoints.pndServiceGetPndList.initiate(payload))
+    subscription.unwrap().then((response) => {
+        thunkApi.dispatch(setPnds(response.pnd))
+    })
+})
\ No newline at end of file
diff --git a/react-ui/src/components/devices/reducer/device.reducer.ts b/react-ui/src/components/devices/reducer/device.reducer.ts
index 8e4454bd2..f574509be 100755
--- a/react-ui/src/components/devices/reducer/device.reducer.ts
+++ b/react-ui/src/components/devices/reducer/device.reducer.ts
@@ -1,12 +1,10 @@
 import {
-    api,
     NetworkelementFlattenedManagedNetworkElement,
     NetworkelementManagedNetworkElement,
-    PndPrincipalNetworkDomain,
-    PndServiceGetPndListApiArg,
+    PndPrincipalNetworkDomain
 } from '@api/api'
 import { DeviceViewTabValues } from '@component/devices/view/device.view.tabs'
-import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
 import { RootState } from 'src/stores'
 import '../routines/index'
 import { startListening } from '/src/stores/middleware/listener.middleware'
@@ -40,11 +38,11 @@ const deviceSlice = createSlice({
     name: 'device',
     initialState,
     reducers: {
-        setDevices: (state, action: PayloadAction<Device[]>) => {
-            state.devices = action.payload
+        setDevices: (state, action: PayloadAction<Device[] | undefined>) => {
+            state.devices = action.payload || []
         },
-        setPnds: (state, action: PayloadAction<PndPrincipalNetworkDomain[]>) => {
-            state.pnds = action.payload
+        setPnds: (state, action: PayloadAction<PndPrincipalNetworkDomain[] | undefined>) => {
+            state.pnds = action.payload || []
         },
         setActiveTab: (state, action: PayloadAction<DeviceViewTabValues>) => {
             state.activeTab = action.payload
@@ -77,31 +75,23 @@ const deviceSlice = createSlice({
     },
 })
 
-export const { setDevices, setActiveTab, setSelectedDevice, setSelectedMne, setSelectedJson } =
+export const { setDevices, setActiveTab, setSelectedDevice, setSelectedMne, setSelectedJson, setPnds } =
     deviceSlice.actions
-const { setPnds } = deviceSlice.actions
 
 export default deviceSlice.reducer
 export const deviceReducerPath = deviceSlice.reducerPath
 
-export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
-    const payload: PndServiceGetPndListApiArg = {
-        timestamp: new Date().getTime().toString(),
-    }
-
-    const subscription = thunkApi.dispatch(api.endpoints.pndServiceGetPndList.initiate(payload))
-    subscription.unwrap().then((response) => {
-        thunkApi.dispatch(setPnds(response.pnd))
-    })
-})
-
 // add default selected device if no selected device is set
 startListening({
     predicate: (action) => setDevices.match(action),
     effect: async (action, listenerApi) => {
-        const { device } = listenerApi.getOriginalState() as RootState
-        if (!device.selectedDevice && !!action.payload[0]) {
-            listenerApi.dispatch(setSelectedDevice(action.payload[0]))
+        const { device: state } = listenerApi.getOriginalState() as RootState
+        if (state.selectedDevice) {
+            return
         }
+
+        // if there are no devices available do set null
+        const newDevices = action.payload?.[0] || null
+        listenerApi.dispatch(setSelectedDevice(newDevices))
     },
 })
diff --git a/react-ui/src/index.tsx b/react-ui/src/index.tsx
index 8383248ce..3697efd07 100755
--- a/react-ui/src/index.tsx
+++ b/react-ui/src/index.tsx
@@ -16,6 +16,7 @@ import { router } from './routes'
 import './shared/icons/icons'
 import { persistor, store } from './stores'
 
+window.env = window.location.hostname === 'localhost' ? 'development' : 'production';
 
 ReactDOM.createRoot(document.getElementById("root")).render(
     <React.StrictMode>
diff --git a/react-ui/src/shared/api/user.fetch.ts b/react-ui/src/shared/api/user.fetch.ts
new file mode 100644
index 000000000..1b6dd344e
--- /dev/null
+++ b/react-ui/src/shared/api/user.fetch.ts
@@ -0,0 +1,25 @@
+import { createAsyncThunk } from "@reduxjs/toolkit"
+import { setUser } from "@shared/reducer/user.reducer"
+import { RootState } from "src/stores"
+import { api, UserServiceGetUsersApiArg } from "./api"
+
+export const fetchUser = createAsyncThunk('user/fetchUser', (_, thunkAPI) => {
+    const payload: UserServiceGetUsersApiArg = {}
+
+    thunkAPI.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => {
+        if (response.error || !response.data?.user?.length) {
+            // TODO proper error handling
+            throw new Error('Fetching the pnd list after successful login failed')
+        }
+
+        const { user } = thunkAPI.getState() as RootState
+        const matchedUser = response.data.user.find((_user) => _user.name === user.username)
+
+        if (!matchedUser) {
+            // TODO proper error handling
+            throw new Error('No user found with the provided username')
+        }
+
+        thunkAPI.dispatch(setUser(matchedUser))
+    })
+})
diff --git a/react-ui/src/shared/helper/debug.ts b/react-ui/src/shared/helper/debug.ts
new file mode 100644
index 000000000..6628989b8
--- /dev/null
+++ b/react-ui/src/shared/helper/debug.ts
@@ -0,0 +1,5 @@
+export const debugMessage = (message: string) => {
+    if (window?.env === 'development') {
+        console.warn("Debug: \n" + message)
+    }
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
index c7e13c6fe..17b6209a5 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
@@ -1,12 +1,12 @@
+import { fetchUser } from '@api/user.fetch';
 import logo from '@assets/logo.svg';
-import { fetchPnds } from '@component/devices/reducer/device.reducer';
+import { fetchPnds } from '@component/devices/api/pnd.fetch';
 import { faCircleUser, faRightFromBracket } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { useAppDispatch, useAppSelector } from '@hooks';
 import { useAuth } from "@provider/auth.provider";
 import { MenuProvider } from '@provider/menu/menu.provider';
 import { DEVICE_URL, LOGIN_URL } from '@routes';
-import { fetchUser } from '@shared/reducer/user.reducer';
 import React, { useEffect } from "react";
 import { Dropdown } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
diff --git a/react-ui/src/shared/reducer/routine.reducer.ts b/react-ui/src/shared/reducer/routine.reducer.ts
index a52ee84d3..fc5a4b35b 100755
--- a/react-ui/src/shared/reducer/routine.reducer.ts
+++ b/react-ui/src/shared/reducer/routine.reducer.ts
@@ -1,33 +1,43 @@
+import { debugMessage } from '@helper/debug'
 import { PayloadAction, createSlice } from '@reduxjs/toolkit'
 import { RoutineManager } from '@utils/routine.manager'
 import { RootState } from '../../stores'
 import { startListening } from '../../stores/middleware/listener.middleware'
 import { setToken } from './user.reducer'
 
+// ---------------- thunk types ---------------- 
+
 interface ThunkEntityDTO {
     thunk: any
     payload: Object
 
     /**
-     * Only one subscription per category is allowed. New subscription will unsubscribe and overwrite the old one
+     * Only one subscription per category is allowed.
+     * New subscription will unsubscribe and overwrite the old one
      */
     category: CATEGORIES
 }
 
-interface ThunkEntity extends ThunkEntityDTO {
+/**
+ *  This Wrapper holds the actual thunk information 
+ *  as well as additional information 
+ */
+interface ThunkWrapper extends ThunkEntityDTO {
     id?: number
     locked: boolean
 }
 
-export interface ReducerState {
-    thunks: { [key in keyof typeof CATEGORIES]: ThunkEntity | null }
-}
-
 export enum CATEGORIES {
     TABLE,
     TAB,
 }
 
+// ---------------- reducer types ---------------- 
+
+export interface ReducerState {
+    thunks: { [key in keyof typeof CATEGORIES]: ThunkWrapper | null }
+}
+
 const initialState: ReducerState = {
     thunks: {
         TABLE: null,
@@ -40,7 +50,11 @@ const RoutineSlice = createSlice({
     initialState,
     reducers: {
         addRoutine: (state: any, { payload }: PayloadAction<ThunkEntityDTO>) => {
-            const newThunk: ThunkEntity = { ...payload, locked: true }
+            if (state.thunks[CATEGORIES[payload.category]]?.locked) {
+
+            }
+
+            const newThunk: ThunkWrapper = { ...payload, locked: true }
             state.thunks[CATEGORIES[payload.category]] = newThunk
         },
 
@@ -48,8 +62,8 @@ const RoutineSlice = createSlice({
             const thunk = state.thunks[CATEGORIES[payload.category] as any]
 
             if (!thunk) {
-                // TODO
-                throw new Error('Thunk not found')
+                debugMessage("Desired thunk of category " + payload.category + " is not available")
+                return
             }
 
             state.thunks[CATEGORIES[payload.category] as any] = { ...thunk, id: payload.id, locked: false }
@@ -89,11 +103,17 @@ startListening({
 //     },
 // })
 
-// add new routine
+/**
+ * Add new routine
+ * 
+ * This listener handles the connection between the RoutingManager that 
+ * stores the non persistable thunk object and the peristable thunk information.
+ * The persistable information are stored in this reducer
+ */
 startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
-        const { thunk } = action.payload as ThunkEntity
+        const { thunk } = action.payload as ThunkWrapper
         const subscription = await listenerApi.dispatch(thunk(action.payload.payload))
         const thunkId = await RoutineManager.add(subscription.payload)
         listenerApi.dispatch(
diff --git a/react-ui/src/shared/reducer/user.reducer.ts b/react-ui/src/shared/reducer/user.reducer.ts
index af0f2d171..a0f2d4222 100755
--- a/react-ui/src/shared/reducer/user.reducer.ts
+++ b/react-ui/src/shared/reducer/user.reducer.ts
@@ -1,7 +1,6 @@
-import { api, RbacUser, UserServiceGetUsersApiArg } from '@api/api'
+import { RbacUser } from '@api/api'
 import { setCookieValue } from '@helper/coookie'
-import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
-import { RootState } from '..'
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
 
 export interface UserSliceState {
     // defined by the frontend user input. This value is getting compared with the backend response
@@ -34,27 +33,4 @@ export const { setToken } = userSlice.actions
 export const { setUser } = userSlice.actions
 
 export default userSlice.reducer
-export const userReducerPath = userSlice.reducerPath
-
-export const fetchUser = createAsyncThunk('user/fetchUser', (_, thunkAPI) => {
-    const payload: UserServiceGetUsersApiArg = {}
-
-    thunkAPI.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => {
-        if (response.error || !response.data?.user?.length) {
-            // TODO proper error handling
-            throw new Error('Fetching the pnd list after successful login failed')
-        }
-
-        const { user } = thunkAPI.getState() as RootState
-
-        // TODO ask if this is the correct approach
-        const matchedUser = response.data.user.find((_user) => _user.name === user.username)
-
-        if (!matchedUser) {
-            // TODO proper error handling
-            throw new Error('No user found with the provided username')
-        }
-
-        thunkAPI.dispatch(setUser(matchedUser))
-    })
-})
+export const userReducerPath = userSlice.reducerPath
\ No newline at end of file
diff --git a/react-ui/src/shared/utils/routine.manager.ts b/react-ui/src/shared/utils/routine.manager.ts
index ade079289..2f1a8086a 100755
--- a/react-ui/src/shared/utils/routine.manager.ts
+++ b/react-ui/src/shared/utils/routine.manager.ts
@@ -14,7 +14,7 @@ const initialState = {
 /**
  * Routine manager is a singleton that holds all running routines.
  * The redux store holds any persistable information about the routines.
- * The routines objects itself are stored in the RoutineManager.
+ * The routine objects itself are stored in the RoutineManager.
  */
 export const RoutineManager = (() => {
     const state = initialState
-- 
GitLab


From 4c7056c81a3bb5abdaba075ab85d261dc3560579 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Wed, 8 Jan 2025 12:55:13 +0100
Subject: [PATCH 20/45] (ui): change routine identification to category

---
 .../src/components/devices/api/pnd.fetch.ts   |  1 +
 .../devices/reducer/device.reducer.ts         | 34 +++++----
 .../devices/routines/device.routine.ts        |  7 +-
 .../devices/routines/mne.routine.ts           | 32 ++++++--
 .../devices/view/device.view.table.tsx        |  2 +-
 .../devices/view/device.view.tabs.tsx         |  2 +-
 .../view_model/device.tabs.viewmodel.ts       |  2 +-
 .../src/components/login/view/login.view.tsx  |  2 +-
 .../json_viewer/view/json_viewer.view.tsx     |  2 +-
 react-ui/src/shared/helper/debug.ts           |  8 +-
 .../src/shared/provider/auth.provider.tsx     |  2 +-
 .../shared/provider/menu/menu.provider.tsx    |  2 +-
 .../src/shared/provider/utils.provider.tsx    |  2 +-
 .../src/shared/reducer/routine.reducer.ts     | 75 ++++--------------
 react-ui/src/shared/types/category.type.ts    | 17 +++++
 .../interfaces.type.ts}                       |  0
 react-ui/src/shared/types/thunk.type.ts       | 12 +++
 react-ui/src/shared/utils/routine.manager.ts  | 76 ++++++++++---------
 18 files changed, 151 insertions(+), 127 deletions(-)
 create mode 100644 react-ui/src/shared/types/category.type.ts
 rename react-ui/src/shared/{helper/interfaces.ts => types/interfaces.type.ts} (100%)
 create mode 100644 react-ui/src/shared/types/thunk.type.ts

diff --git a/react-ui/src/components/devices/api/pnd.fetch.ts b/react-ui/src/components/devices/api/pnd.fetch.ts
index 6e677ab8d..fd49de636 100644
--- a/react-ui/src/components/devices/api/pnd.fetch.ts
+++ b/react-ui/src/components/devices/api/pnd.fetch.ts
@@ -2,6 +2,7 @@ import { PndServiceGetPndListApiArg, api } from "@api/api"
 import { createAsyncThunk } from "@reduxjs/toolkit"
 import { setPnds } from "../reducer/device.reducer"
 
+// TODO rethink this. This should be in the shared part bc its getting invoked in the procteded layout
 export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
     const payload: PndServiceGetPndListApiArg = {
         timestamp: new Date().getTime().toString(),
diff --git a/react-ui/src/components/devices/reducer/device.reducer.ts b/react-ui/src/components/devices/reducer/device.reducer.ts
index f574509be..9d91700c4 100755
--- a/react-ui/src/components/devices/reducer/device.reducer.ts
+++ b/react-ui/src/components/devices/reducer/device.reducer.ts
@@ -11,27 +11,25 @@ import { startListening } from '/src/stores/middleware/listener.middleware'
 
 export type Device = NetworkelementFlattenedManagedNetworkElement
 
-interface SelectedDeviceInterface {
+interface SelectedObject {
     device: Device
     mne: NetworkelementManagedNetworkElement | null
     json: JSON | null
 }
 
-type SelectedDeviceType = SelectedDeviceInterface | null
-
 export interface DeviceSliceState {
     devices: Device[]
     pnds: PndPrincipalNetworkDomain[]
 
     activeTab: DeviceViewTabValues
-    selectedDevice: SelectedDeviceType
+    selected: SelectedObject | null
 }
 
 const initialState: DeviceSliceState = {
     devices: [],
     pnds: [],
     activeTab: DeviceViewTabValues.METADATA,
-    selectedDevice: null,
+    selected: null,
 }
 
 const deviceSlice = createSlice({
@@ -48,29 +46,33 @@ const deviceSlice = createSlice({
             state.activeTab = action.payload
         },
         setSelectedDevice: (state, action: PayloadAction<Device | null>) => {
-            let selectedDevice: SelectedDeviceType = null
+            let selectedObject = null;
             if (action.payload) {
-                selectedDevice = { device: action.payload, mne: null, json: null }
+                selectedObject = { device: action.payload, mne: null, json: null }
             }
 
-            state.selectedDevice = selectedDevice
+            state.selected = selectedObject
         },
         setSelectedMne: (state, action: PayloadAction<NetworkelementManagedNetworkElement>) => {
-            if (!state.selectedDevice) {
-                throw new Error('Selected Device is null where it shouldn´t be null')
+            if (!state.selected) {
+                throw new Error('Can not find corresponding device')
+            }
+
+            // safety check to prevent possible race conditions
+            if (state.selected.device.id !== action.payload.id) {
+                // TODO proper error handling by retry fetching the device object
+                throw new Error('Device and mne id does not match')
             }
 
-            state.selectedDevice.mne = action.payload
-            // TODO maybe don´t take the device of "selected device" instead search in the devices array for the proper device
-            // should not make a diffrence due to pointer but dunno
+            state.selected.mne = action.payload
         },
 
         setSelectedJson: (state, action: PayloadAction<JSON>) => {
-            if (!state.selectedDevice) {
+            if (!state.selected) {
                 throw new Error('Selected Device is null where it shouldn´t be null')
             }
 
-            state.selectedDevice.json = action.payload || null
+            state.selected.json = action.payload || null
         },
     },
 })
@@ -86,7 +88,7 @@ startListening({
     predicate: (action) => setDevices.match(action),
     effect: async (action, listenerApi) => {
         const { device: state } = listenerApi.getOriginalState() as RootState
-        if (state.selectedDevice) {
+        if (state.selected) {
             return
         }
 
diff --git a/react-ui/src/components/devices/routines/device.routine.ts b/react-ui/src/components/devices/routines/device.routine.ts
index 10caffa2f..f9b3e1ee2 100755
--- a/react-ui/src/components/devices/routines/device.routine.ts
+++ b/react-ui/src/components/devices/routines/device.routine.ts
@@ -19,8 +19,13 @@ startListening({
 export const fetchDevicesThunk = createAsyncThunk(FETCH_DEVICE_ACTION, (_, thunkApi) => {
     const { user } = thunkApi.getState() as RootState
 
+    if (!user.user?.roles) {
+        throw new Error('Background MNE fetching failed! User data is missing. Reload page or logout and login again')
+        // TODO
+    }
+
     const payload: NetworkElementServiceGetAllFlattenedApiArg = {
-        pid: Object.keys(user?.user.roles)[0],
+        pid: Object.keys(user.user.roles)[0],
         timestamp: new Date().getTime().toString(),
     }
 
diff --git a/react-ui/src/components/devices/routines/mne.routine.ts b/react-ui/src/components/devices/routines/mne.routine.ts
index 7de1d1814..a3ea43d2f 100755
--- a/react-ui/src/components/devices/routines/mne.routine.ts
+++ b/react-ui/src/components/devices/routines/mne.routine.ts
@@ -6,26 +6,38 @@ import {
     setSelectedMne,
 } from '@component/devices/reducer/device.reducer'
 import { createAsyncThunk } from '@reduxjs/toolkit'
-import { addRoutine, CATEGORIES } from '@shared/reducer/routine.reducer'
+import { addRoutine } from '@shared/reducer/routine.reducer'
+import { Category } from '@shared/types/category.type'
 import { RootState } from 'src/stores'
 import { startListening } from '../../../stores/middleware/listener.middleware'
 
 export const FETCH_MNE_ACTION = 'subscription/device/fetchSelectedMNE'
 
-// fetch mne if selected device is set
+/**
+ * #0
+ * Trigger fetch MNE (#1)
+ * 
+ * Triggered by a selectedDevice
+ */
 startListening({
     predicate: (action) => setSelectedDevice.match(action) && !!action.payload,
     effect: async (action, listenerApi) => {
         listenerApi.dispatch(
             addRoutine({
                 thunk: fetchSelectedMneThunk,
-                category: CATEGORIES.TAB,
+                category: Category.TAB,
                 payload: action.payload,
             })
         )
     },
 })
 
+/**
+ * #1
+ * Fetch MNE
+ * 
+ * Triggered by #0
+ */
 const FETCH_MNE_INTERVAL = 5000 // in ms
 export const fetchSelectedMneThunk = createAsyncThunk(
     FETCH_MNE_ACTION,
@@ -56,7 +68,12 @@ export const fetchSelectedMneThunk = createAsyncThunk(
     }
 )
 
-// save fetched mne
+/**
+ * #2
+ * Received MNE
+ * 
+ * Triggered by #1
+ */
 startListening({
     predicate: (action) => api.endpoints.networkElementServiceGet.matchFulfilled(action),
     effect: async (action, listenerApi) => {
@@ -64,7 +81,12 @@ startListening({
     },
 })
 
-// save fetched mne
+/**
+ * #3
+ * Fetch & receive json
+ * 
+ * Triggered by #2
+ */
 startListening({
     predicate: (action) => setSelectedMne.match(action),
     effect: async (action, listenerApi) => {
diff --git a/react-ui/src/components/devices/view/device.view.table.tsx b/react-ui/src/components/devices/view/device.view.table.tsx
index 312caab60..9b731fef5 100755
--- a/react-ui/src/components/devices/view/device.view.table.tsx
+++ b/react-ui/src/components/devices/view/device.view.table.tsx
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
 import { useDeviceTableViewModel } from "../view_model/device.table.viewmodel";
 
 export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) => {
-    const { devices, pnds, selectedDevice } = useAppSelector(state => state.device);
+    const { devices, pnds, selected: selectedDevice } = useAppSelector(state => state.device);
     const { t } = useTranslation('common');
     const { trClickHandler } = useDeviceTableViewModel(searchRef);
 
diff --git a/react-ui/src/components/devices/view/device.view.tabs.tsx b/react-ui/src/components/devices/view/device.view.tabs.tsx
index 2929f9b64..a2768a0ea 100755
--- a/react-ui/src/components/devices/view/device.view.tabs.tsx
+++ b/react-ui/src/components/devices/view/device.view.tabs.tsx
@@ -8,7 +8,7 @@ export enum DeviceViewTabValues {
 }
 
 export const DeviceViewTabs = (activeTab: DeviceViewTabValues) => {
-    const { selectedDevice } = useAppSelector(state => state.device);
+    const { selected: selectedDevice } = useAppSelector(state => state.device);
     const { jsonYang } = useDeviceTabsViewModel();
 
     const metadataTab = () => {
diff --git a/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts b/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
index 4a60567b6..af4cc3abb 100755
--- a/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
@@ -7,7 +7,7 @@ export enum DeviceViewTabValues {
 }
 
 export const useDeviceTabsViewModel = () => {
-    const { selectedDevice } = useAppSelector((state) => state.device)
+    const { selected: selectedDevice } = useAppSelector((state) => state.device)
 
     const getYangModelJSON = (): JSON | null => {
         if (!selectedDevice?.json) {
diff --git a/react-ui/src/components/login/view/login.view.tsx b/react-ui/src/components/login/view/login.view.tsx
index 03d7406f4..38afc83a1 100755
--- a/react-ui/src/components/login/view/login.view.tsx
+++ b/react-ui/src/components/login/view/login.view.tsx
@@ -1,5 +1,5 @@
 import logo from '@assets/logo.svg'
-import { BasicProp } from '@helper/interfaces'
+import { BasicProp } from '@shared/types/interfaces.type'
 import React, { useRef } from 'react'
 import { Alert, Button, Col, Container, Form, Image, Row, Spinner } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
diff --git a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
index 720f75c7d..b8358c686 100755
--- a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
+++ b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
@@ -130,7 +130,7 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
     const searchHTML = () => {
         return (
             <>
-                <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'>
+                <Form.Group controlId='json_viewer.search' className='p-0 mx-1 pt-2'>
                     <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={search} />
                 </Form.Group>
             </>
diff --git a/react-ui/src/shared/helper/debug.ts b/react-ui/src/shared/helper/debug.ts
index 6628989b8..db10a0979 100644
--- a/react-ui/src/shared/helper/debug.ts
+++ b/react-ui/src/shared/helper/debug.ts
@@ -1,5 +1,11 @@
-export const debugMessage = (message: string) => {
+export const warnMessage = (message: string) => {
     if (window?.env === 'development') {
         console.warn("Debug: \n" + message)
     }
+}
+
+export const infoMessage = (message: string) => {
+    if (window?.env === 'development') {
+        console.info("Info: \n" + message)
+    }
 }
\ No newline at end of file
diff --git a/react-ui/src/shared/provider/auth.provider.tsx b/react-ui/src/shared/provider/auth.provider.tsx
index 69bdccbdf..77219bdce 100755
--- a/react-ui/src/shared/provider/auth.provider.tsx
+++ b/react-ui/src/shared/provider/auth.provider.tsx
@@ -1,8 +1,8 @@
 import { AuthServiceLoginApiArg, AuthServiceLoginApiResponse, useAuthServiceLoginMutation } from "@api/api";
 import { getCookieValue } from "@helper/coookie";
-import { BasicProp } from "@helper/interfaces";
 import { useAppDispatch, useAppSelector } from "@hooks";
 import { DEVICE_URL, LOGIN_URL } from "@routes";
+import { BasicProp } from "@shared/types/interfaces.type";
 import { jwtDecode } from "jwt-decode";
 import { createContext, useContext, useEffect, useMemo } from "react";
 import { useNavigate } from "react-router-dom";
diff --git a/react-ui/src/shared/provider/menu/menu.provider.tsx b/react-ui/src/shared/provider/menu/menu.provider.tsx
index a91f46639..b2525692b 100644
--- a/react-ui/src/shared/provider/menu/menu.provider.tsx
+++ b/react-ui/src/shared/provider/menu/menu.provider.tsx
@@ -1,7 +1,7 @@
 import { faRightFromBracket, IconDefinition } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { BasicProp } from "@helper/interfaces";
 import { useAuth } from "@provider/auth.provider";
+import { BasicProp } from "@shared/types/interfaces.type";
 import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
 import { useTranslation } from "react-i18next";
 import './menu.provider.scss';
diff --git a/react-ui/src/shared/provider/utils.provider.tsx b/react-ui/src/shared/provider/utils.provider.tsx
index ca6aa1d32..3a76bbe69 100644
--- a/react-ui/src/shared/provider/utils.provider.tsx
+++ b/react-ui/src/shared/provider/utils.provider.tsx
@@ -1,4 +1,4 @@
-import { BasicProp } from "@helper/interfaces";
+import { BasicProp } from "@shared/types/interfaces.type";
 import React, { createContext, useContext, useMemo } from "react";
 import { useTranslation } from "react-i18next";
 import { toast } from "react-toastify";
diff --git a/react-ui/src/shared/reducer/routine.reducer.ts b/react-ui/src/shared/reducer/routine.reducer.ts
index fc5a4b35b..873d7d6a2 100755
--- a/react-ui/src/shared/reducer/routine.reducer.ts
+++ b/react-ui/src/shared/reducer/routine.reducer.ts
@@ -1,72 +1,31 @@
-import { debugMessage } from '@helper/debug'
 import { PayloadAction, createSlice } from '@reduxjs/toolkit'
+import { CategoryType } from '@shared/types/category.type'
+import { ThunkEntityDTO } from '@shared/types/thunk.type'
 import { RoutineManager } from '@utils/routine.manager'
 import { RootState } from '../../stores'
 import { startListening } from '../../stores/middleware/listener.middleware'
 import { setToken } from './user.reducer'
 
-// ---------------- thunk types ---------------- 
-
-interface ThunkEntityDTO {
-    thunk: any
-    payload: Object
-
-    /**
-     * Only one subscription per category is allowed.
-     * New subscription will unsubscribe and overwrite the old one
-     */
-    category: CATEGORIES
-}
-
-/**
- *  This Wrapper holds the actual thunk information 
- *  as well as additional information 
- */
-interface ThunkWrapper extends ThunkEntityDTO {
-    id?: number
-    locked: boolean
-}
-
-export enum CATEGORIES {
-    TABLE,
-    TAB,
-}
-
-// ---------------- reducer types ---------------- 
-
 export interface ReducerState {
-    thunks: { [key in keyof typeof CATEGORIES]: ThunkWrapper | null }
+    thunks: Record<CategoryType, ThunkEntityDTO | null>
 }
 
 const initialState: ReducerState = {
     thunks: {
+        DEVICE: null,
         TABLE: null,
-        TAB: null,
+        TAB: null
     },
 }
 
+
 const RoutineSlice = createSlice({
     name: 'routine',
     initialState,
     reducers: {
         addRoutine: (state: any, { payload }: PayloadAction<ThunkEntityDTO>) => {
-            if (state.thunks[CATEGORIES[payload.category]]?.locked) {
-
-            }
-
-            const newThunk: ThunkWrapper = { ...payload, locked: true }
-            state.thunks[CATEGORIES[payload.category]] = newThunk
-        },
-
-        setThunkId: (state, { payload }: PayloadAction<{ id: number; category: CATEGORIES }>) => {
-            const thunk = state.thunks[CATEGORIES[payload.category] as any]
-
-            if (!thunk) {
-                debugMessage("Desired thunk of category " + payload.category + " is not available")
-                return
-            }
-
-            state.thunks[CATEGORIES[payload.category] as any] = { ...thunk, id: payload.id, locked: false }
+            const newThunk: ThunkEntityDTO = payload
+            state.thunks[payload.category] = newThunk
         },
 
         removeAll: (state) => {
@@ -113,12 +72,9 @@ startListening({
 startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
-        const { thunk } = action.payload as ThunkWrapper
+        const { thunk } = action.payload as ThunkEntityDTO
         const subscription = await listenerApi.dispatch(thunk(action.payload.payload))
-        const thunkId = await RoutineManager.add(subscription.payload)
-        listenerApi.dispatch(
-            RoutineSlice.actions.setThunkId({ id: thunkId, category: action.payload.category })
-        )
+        RoutineManager.add(subscription.payload, action.payload.category)
     },
 })
 
@@ -127,14 +83,11 @@ startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
         const { routine } = listenerApi.getOriginalState() as RootState
-        const lastThunk = routine.thunks[CATEGORIES[action.payload.category] as any]
-        if (lastThunk) {
-            if (!lastThunk.id) {
-                throw new Error()
-                // TODO
-            }
+        const category = action.payload.category;
 
-            RoutineManager.unsubscribe(lastThunk.id)
+        const lastThunk = routine.thunks[category as CategoryType]
+        if (lastThunk) {
+            RoutineManager.unsubscribe(category)
         }
     },
 })
diff --git a/react-ui/src/shared/types/category.type.ts b/react-ui/src/shared/types/category.type.ts
new file mode 100644
index 000000000..e08282341
--- /dev/null
+++ b/react-ui/src/shared/types/category.type.ts
@@ -0,0 +1,17 @@
+
+const DeviceListView = {
+    TABLE: "device_list/table",
+    TAB: "device_list/tab",
+}
+
+const Shared = {
+    DEVICE: 'objects/device'
+}
+
+
+export const Category = {
+    ...DeviceListView,
+    ...Shared
+}
+
+export type CategoryType = keyof typeof Category
\ No newline at end of file
diff --git a/react-ui/src/shared/helper/interfaces.ts b/react-ui/src/shared/types/interfaces.type.ts
similarity index 100%
rename from react-ui/src/shared/helper/interfaces.ts
rename to react-ui/src/shared/types/interfaces.type.ts
diff --git a/react-ui/src/shared/types/thunk.type.ts b/react-ui/src/shared/types/thunk.type.ts
new file mode 100644
index 000000000..ef0a92b87
--- /dev/null
+++ b/react-ui/src/shared/types/thunk.type.ts
@@ -0,0 +1,12 @@
+import { CategoryType } from "./category.type"
+
+export interface ThunkEntityDTO {
+    thunk: any
+    payload: Object
+
+    /**
+     * Only one subscription per category is allowed.
+     * New subscription will unsubscribe and overwrite the old one
+     */
+    category: CategoryType
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/utils/routine.manager.ts b/react-ui/src/shared/utils/routine.manager.ts
index 2f1a8086a..427043e24 100755
--- a/react-ui/src/shared/utils/routine.manager.ts
+++ b/react-ui/src/shared/utils/routine.manager.ts
@@ -1,67 +1,73 @@
+import { infoMessage, warnMessage } from '@helper/debug'
 import { QueryActionCreatorResult } from '@reduxjs/toolkit/query'
+import { Category, CategoryType } from '@shared/types/category.type'
 
 type Routine = QueryActionCreatorResult<any>
 
 interface Entity {
     routine: Routine
-    id: number
 }
 
-const initialState = {
-    routines: [] as Entity[],
+
+interface RoutineState {
+    routines: Record<CategoryType, Entity | null>
+}
+
+const initalState: RoutineState = {
+    routines: {
+        DEVICE: null,
+        TABLE: null,
+        TAB: null
+    }
 }
 
 /**
- * Routine manager is a singleton that holds all running routines.
- * The redux store holds any persistable information about the routines.
- * The routine objects itself are stored in the RoutineManager.
- */
+* Routine manager is a singleton that holds all running routines.
+* The redux store holds any persistable information about the routines.
+* The routine objects itself are stored in the RoutineManager.
+*/
 export const RoutineManager = (() => {
-    const state = initialState
-    const add = (routine: Routine): number => {
-        const id = state.routines.length
+    let state = initalState
 
-        const newEntity: Entity = {
+    const add = (routine: Routine, category: CategoryType): boolean => {
+        const entity: Entity = {
             routine: routine,
-            id,
         }
 
-        state.routines = [...state.routines, newEntity]
-
-        return id
+        state.routines = {
+            ...state.routines,
+            [category]: entity
+        }
+        return true
     }
 
     const unsubscribeAll = () => {
-        state.routines.forEach(({ routine: subscription }) => {
-            _unsubscribe(subscription)
-        })
+        Object.keys(state.routines)
+            .forEach((category) => {
+                unsubscribe(category as CategoryType)
+            })
 
-        state.routines = initialState.routines
+        state = initalState
     }
 
     /**
      * @param id
      * @returns returns true if the routine was stopped, false if it was not found
      */
-    const unsubscribe = (id: number): boolean => {
-        const routine = state.routines.find(({ id: routineId }) => routineId === id)
+    const unsubscribe = (category: CategoryType): boolean => {
+        const entity = state.routines[category]
 
-        if (routine) {
-            _unsubscribe(routine.routine)
+        if (entity) {
+            entity.routine.unsubscribe()
+            state.routines[category] = null
+            infoMessage("Routine unsubscribed from category " + category)
         }
 
-        return !!routine
-    }
+        if (!!entity) {
+            warnMessage("Desired routine to unsubscribe does not exist in category " + Category[category])
+        }
 
-    /**
-     * Actual unsubscribe process.
-     * This process is extracted to have a single process of unsubscribing.
-     *
-     * @param subscription
-     */
-    const _unsubscribe = (subscription: Routine) => {
-        subscription.unsubscribe()
-        // TODO remove from state
+        return !!entity
     }
 
     return {
@@ -69,4 +75,4 @@ export const RoutineManager = (() => {
         unsubscribe,
         unsubscribeAll,
     }
-})()
+})()
\ No newline at end of file
-- 
GitLab


From d2720ef07c3fdda3d734f989baf5a9238c628c64 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Wed, 8 Jan 2025 15:23:36 +0100
Subject: [PATCH 21/45] (ui): refactor rehydration routine subscriptions

---
 .../devices/reducer/device.reducer.ts         | 26 ++++++---
 .../devices/routines/mne.routine.ts           | 12 +++--
 react-ui/src/index.tsx                        |  8 +++
 react-ui/src/shared/api/user.fetch.ts         |  2 +-
 .../src/shared/reducer/routine.reducer.ts     | 54 ++++++++++++-------
 react-ui/src/shared/types/thunk.type.ts       | 26 +++++++--
 .../shared/utils/routine-holder.singleton.ts  | 47 ++++++++++++++++
 react-ui/src/shared/utils/routine.manager.ts  |  2 +
 8 files changed, 142 insertions(+), 35 deletions(-)
 create mode 100644 react-ui/src/shared/utils/routine-holder.singleton.ts

diff --git a/react-ui/src/components/devices/reducer/device.reducer.ts b/react-ui/src/components/devices/reducer/device.reducer.ts
index 9d91700c4..4afcba788 100755
--- a/react-ui/src/components/devices/reducer/device.reducer.ts
+++ b/react-ui/src/components/devices/reducer/device.reducer.ts
@@ -45,13 +45,27 @@ const deviceSlice = createSlice({
         setActiveTab: (state, action: PayloadAction<DeviceViewTabValues>) => {
             state.activeTab = action.payload
         },
-        setSelectedDevice: (state, action: PayloadAction<Device | null>) => {
-            let selectedObject = null;
-            if (action.payload) {
-                selectedObject = { device: action.payload, mne: null, json: null }
-            }
+        setSelectedDevice: {
+            reducer: (state, action: PayloadAction<Device | null, string, { skipListener?: boolean }>) => {
+                // do thing if desired device is already selected
+                if (state.selected?.device.id === action.payload?.id) {
+                    action.meta.skipListener = true
+                    return
+                }
+
+                let selectedObject = null;
+                if (action.payload) {
+                    selectedObject = { device: action.payload, mne: null, json: null }
+                }
 
-            state.selected = selectedObject
+                state.selected = selectedObject
+            },
+            prepare: (device: Device | null) => {
+                return {
+                    payload: device,
+                    meta: { skipListener: false } // set to true when needed
+                }
+            }
         },
         setSelectedMne: (state, action: PayloadAction<NetworkelementManagedNetworkElement>) => {
             if (!state.selected) {
diff --git a/react-ui/src/components/devices/routines/mne.routine.ts b/react-ui/src/components/devices/routines/mne.routine.ts
index a3ea43d2f..876317dd2 100755
--- a/react-ui/src/components/devices/routines/mne.routine.ts
+++ b/react-ui/src/components/devices/routines/mne.routine.ts
@@ -7,7 +7,8 @@ import {
 } from '@component/devices/reducer/device.reducer'
 import { createAsyncThunk } from '@reduxjs/toolkit'
 import { addRoutine } from '@shared/reducer/routine.reducer'
-import { Category } from '@shared/types/category.type'
+import { Category, CategoryType } from '@shared/types/category.type'
+import { RoutineHolderSingleton } from '@utils/routine-holder.singleton'
 import { RootState } from 'src/stores'
 import { startListening } from '../../../stores/middleware/listener.middleware'
 
@@ -20,13 +21,14 @@ export const FETCH_MNE_ACTION = 'subscription/device/fetchSelectedMNE'
  * Triggered by a selectedDevice
  */
 startListening({
-    predicate: (action) => setSelectedDevice.match(action) && !!action.payload,
+    predicate: (action) => setSelectedDevice.match(action) && !!action.payload && !action.meta?.skipListener,
     effect: async (action, listenerApi) => {
+        const factory = RoutineHolderSingleton.getInstance();
         listenerApi.dispatch(
             addRoutine({
-                thunk: fetchSelectedMneThunk,
-                category: Category.TAB,
-                payload: action.payload,
+                thunk: factory.getRoutineByName("fetchSelectedMneThunk"),
+                category: Category.TAB as CategoryType,
+                payload: action.payload as Object,
             })
         )
     },
diff --git a/react-ui/src/index.tsx b/react-ui/src/index.tsx
index 3697efd07..559e1bc54 100755
--- a/react-ui/src/index.tsx
+++ b/react-ui/src/index.tsx
@@ -1,4 +1,6 @@
+import { fetchSelectedMneThunk } from '@component/devices/routines/mne.routine'
 import { UtilsProvider } from '@provider/utils.provider'
+import { RoutineHolderSingleton } from '@utils/routine-holder.singleton'
 import i18next from 'i18next'
 import React from 'react'
 import ReactDOM from 'react-dom/client'
@@ -18,6 +20,12 @@ import { persistor, store } from './stores'
 
 window.env = window.location.hostname === 'localhost' ? 'development' : 'production';
 
+const factory = RoutineHolderSingleton.getInstance();
+factory.registerRoutine("fetchSelectedMneThunk", {
+    func: fetchSelectedMneThunk,
+    id: 0
+});
+
 ReactDOM.createRoot(document.getElementById("root")).render(
     <React.StrictMode>
         <ErrorBoundary fallback={<div>Something went wrong</div>}>
diff --git a/react-ui/src/shared/api/user.fetch.ts b/react-ui/src/shared/api/user.fetch.ts
index 1b6dd344e..08806783b 100644
--- a/react-ui/src/shared/api/user.fetch.ts
+++ b/react-ui/src/shared/api/user.fetch.ts
@@ -16,7 +16,7 @@ export const fetchUser = createAsyncThunk('user/fetchUser', (_, thunkAPI) => {
         const matchedUser = response.data.user.find((_user) => _user.name === user.username)
 
         if (!matchedUser) {
-            // TODO proper error handling
+            // TODO proper error handling => logout
             throw new Error('No user found with the provided username')
         }
 
diff --git a/react-ui/src/shared/reducer/routine.reducer.ts b/react-ui/src/shared/reducer/routine.reducer.ts
index 873d7d6a2..f8c8c31eb 100755
--- a/react-ui/src/shared/reducer/routine.reducer.ts
+++ b/react-ui/src/shared/reducer/routine.reducer.ts
@@ -1,13 +1,15 @@
 import { PayloadAction, createSlice } from '@reduxjs/toolkit'
 import { CategoryType } from '@shared/types/category.type'
-import { ThunkEntityDTO } from '@shared/types/thunk.type'
+import { ThunkDTO, ThunkPersist } from '@shared/types/thunk.type'
+import { RoutineHolderSingleton } from '@utils/routine-holder.singleton'
 import { RoutineManager } from '@utils/routine.manager'
+import { REHYDRATE } from 'redux-persist'
 import { RootState } from '../../stores'
 import { startListening } from '../../stores/middleware/listener.middleware'
 import { setToken } from './user.reducer'
 
 export interface ReducerState {
-    thunks: Record<CategoryType, ThunkEntityDTO | null>
+    thunks: Record<CategoryType, ThunkPersist | null>
 }
 
 const initialState: ReducerState = {
@@ -23,9 +25,14 @@ const RoutineSlice = createSlice({
     name: 'routine',
     initialState,
     reducers: {
-        addRoutine: (state: any, { payload }: PayloadAction<ThunkEntityDTO>) => {
-            const newThunk: ThunkEntityDTO = payload
-            state.thunks[payload.category] = newThunk
+        addRoutine: (state: any, { payload }: PayloadAction<ThunkDTO>) => {
+            const thunk: ThunkPersist = {
+                category: payload.category,
+                payload: payload.payload,
+                thunkId: payload.thunk.id
+            }
+
+            state.thunks[payload.category] = thunk
         },
 
         removeAll: (state) => {
@@ -48,19 +55,26 @@ startListening({
 // on rehydrate add all persistet routines
 // TODO -> thunk does not have the thunk function object due to its coming from the store that ignores the value.
 // at this point we have to figure out how to get the thunk function out of the "string" name
-// startListening({
-//     predicate: ({ type }) => type === REHYDRATE,
-//     effect: async (_, listenerApi) => {
-//         const { routine } = listenerApi.getState() as RootState
-//         for (const [_, thunk] of Object.entries<ThunkEntity>(routine.thunks)) {
-//             if (!thunk) {
-//                 continue
-//             }
-//             const dto: ThunkEntityDTO = thunk
-//             listenerApi.dispatch(addRoutine(dto))
-//         }
-//     },
-// })
+startListening({
+    predicate: ({ type }) => type === REHYDRATE,
+    effect: async (_, listenerApi) => {
+        const { routine } = listenerApi.getState() as RootState
+        const routines = RoutineHolderSingleton.getInstance()
+
+        Object.values(routine.thunks)
+            .filter(thunk => !!thunk)
+            .forEach(thunk => {
+                const container = routines.getRoutineById(thunk.thunkId)
+                const dto: ThunkDTO = {
+                    category: thunk.category,
+                    payload: thunk.payload,
+                    thunk: container
+                }
+
+                listenerApi.dispatch(addRoutine(dto))
+            })
+    },
+})
 
 /**
  * Add new routine
@@ -72,8 +86,8 @@ startListening({
 startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
-        const { thunk } = action.payload as ThunkEntityDTO
-        const subscription = await listenerApi.dispatch(thunk(action.payload.payload))
+        const { thunk } = action.payload as ThunkDTO
+        const subscription = await listenerApi.dispatch(thunk.func(action.payload.payload))
         RoutineManager.add(subscription.payload, action.payload.category)
     },
 })
diff --git a/react-ui/src/shared/types/thunk.type.ts b/react-ui/src/shared/types/thunk.type.ts
index ef0a92b87..90b846039 100644
--- a/react-ui/src/shared/types/thunk.type.ts
+++ b/react-ui/src/shared/types/thunk.type.ts
@@ -1,7 +1,21 @@
 import { CategoryType } from "./category.type"
 
-export interface ThunkEntityDTO {
-    thunk: any
+
+/**
+ * Contains the thunk function combined with a unique id
+ * Giving a explicit id (and not the index of the object)
+ * prevents missmatching the function if a update changes
+ * the RoutineList object length 
+ */
+export interface ThunkContainer {
+    id: number
+    func: any,
+}
+
+
+export interface ThunkDTO {
+    thunk: ThunkContainer
+
     payload: Object
 
     /**
@@ -9,4 +23,10 @@ export interface ThunkEntityDTO {
      * New subscription will unsubscribe and overwrite the old one
      */
     category: CategoryType
-}
\ No newline at end of file
+}
+
+export interface ThunkPersist {
+    thunkId: number,
+    payload: Object
+    category: CategoryType
+}
diff --git a/react-ui/src/shared/utils/routine-holder.singleton.ts b/react-ui/src/shared/utils/routine-holder.singleton.ts
new file mode 100644
index 000000000..47332ab0b
--- /dev/null
+++ b/react-ui/src/shared/utils/routine-holder.singleton.ts
@@ -0,0 +1,47 @@
+import { ThunkContainer } from "@shared/types/thunk.type";
+
+interface LocalThunkContainer {
+    container: ThunkContainer,
+    name: string
+}
+
+export class RoutineHolderSingleton {
+    private static instance: RoutineHolderSingleton;
+    private routineList: Array<LocalThunkContainer> = []
+
+    private constructor() { }
+
+    static getInstance(): RoutineHolderSingleton {
+        if (!RoutineHolderSingleton.instance) {
+            RoutineHolderSingleton.instance = new RoutineHolderSingleton();
+        }
+        return RoutineHolderSingleton.instance;
+    }
+
+    registerRoutine(name: string, thunk: ThunkContainer) {
+        this.routineList = [...this.routineList, { container: thunk, name }];
+    }
+
+    getRoutineById(id: number): ThunkContainer {
+        const routine = this.routineList.find((thunk) => thunk.container.id === id)
+
+        if (!routine) {
+            throw new Error('')
+            // TODO
+        }
+
+        return routine.container;
+    }
+
+
+    getRoutineByName(name: string): ThunkContainer {
+        const routine = this.routineList.find((thunk) => thunk.name === name)
+
+        if (!routine) {
+            throw new Error('')
+            // TODO
+        }
+
+        return routine.container;
+    }
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/utils/routine.manager.ts b/react-ui/src/shared/utils/routine.manager.ts
index 427043e24..1fee6723e 100755
--- a/react-ui/src/shared/utils/routine.manager.ts
+++ b/react-ui/src/shared/utils/routine.manager.ts
@@ -38,6 +38,8 @@ export const RoutineManager = (() => {
             ...state.routines,
             [category]: entity
         }
+        infoMessage("Routine subscribed to category " + category)
+
         return true
     }
 
-- 
GitLab


From 26b786918a85d2a801cd55297980d5d97180679f Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Wed, 8 Jan 2025 16:54:39 +0100
Subject: [PATCH 22/45] (ui): chunked bundles size | improved nginx config |
 further performance improvements

---
 react-ui/docker/webserver/Dockerfile          |   6 +-
 react-ui/docker/webserver/nginx.conf          | 142 +++++++++---------
 react-ui/package.json                         |  64 ++++----
 react-ui/public/fonts/inter-webfont.woff      | Bin 0 -> 26796 bytes
 react-ui/public/fonts/inter-webfont.woff2     | Bin 0 -> 21204 bytes
 .../components/devices/view/device.view.tsx   |   2 +-
 .../components/login/layouts/login.layout.tsx |   4 +-
 react-ui/src/routes.tsx                       |  34 ++++-
 react-ui/src/shared/icons/icons.ts            |   4 +-
 react-ui/src/shared/style/fonts.scss          |   9 +-
 react-ui/vite.config.mjs                      |  28 +++-
 react-ui/yarn.lock                            |  61 ++++++++
 12 files changed, 232 insertions(+), 122 deletions(-)
 create mode 100644 react-ui/public/fonts/inter-webfont.woff
 create mode 100644 react-ui/public/fonts/inter-webfont.woff2

diff --git a/react-ui/docker/webserver/Dockerfile b/react-ui/docker/webserver/Dockerfile
index 71127c09b..9c4f3f676 100644
--- a/react-ui/docker/webserver/Dockerfile
+++ b/react-ui/docker/webserver/Dockerfile
@@ -3,7 +3,11 @@ FROM node:alpine3.20 as builder
 COPY  ./api/openapiv2/gosdn_northbound.swagger.json /app/api/openapiv2/gosdn_northbound.swagger.json
 COPY ./react-ui /app/react-ui
 
-RUN cd /app/react-ui && yarn && yarn build
+RUN cd /app/react-ui && \
+    rm -rf node_modules && \
+    rm yarn.lock && \
+    yarn install --production && \
+    yarn build
 
 
 # webserver
diff --git a/react-ui/docker/webserver/nginx.conf b/react-ui/docker/webserver/nginx.conf
index eb4fc2be9..4ddf7f20d 100644
--- a/react-ui/docker/webserver/nginx.conf
+++ b/react-ui/docker/webserver/nginx.conf
@@ -1,51 +1,82 @@
-
-#user  nobody;
 worker_processes  1;
 
-#error_log  logs/error.log;
-#error_log  logs/error.log  notice;
-#error_log  logs/error.log  info;
-
-#pid        logs/nginx.pid;
-
-
 events {
     worker_connections  1024;
+    multi_accept on;
+    use epoll;
 }
 
 http {
     include       mime.types;
     default_type  application/octet-stream;
 
-    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
-    #                  '$status $body_bytes_sent "$http_referer" '
-    #                  '"$http_user_agent" "$http_x_forwarded_for"';
-
-    #access_log  logs/access.log  main;
-
-    sendfile        on;
-    #tcp_nopush     on;
-
-    #keepalive_timeout  0;
-    keepalive_timeout  65;
+    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+                    '$status $body_bytes_sent "$http_referer" '
+                    '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log /var/log/nginx/access.log main buffer=16k;
+
+    sendfile on;
+    tcp_nopush on;
+    tcp_nodelay on;
+    keepalive_timeout 65;
+    types_hash_max_size 2048;
+    server_tokens off;
+
+    # Buffer size settings
+    client_body_buffer_size 10K;
+    client_header_buffer_size 1k;
+    client_max_body_size 8m;
+    large_client_header_buffers 2 1k;
+
+    # File descriptor cache
+    open_file_cache max=2000 inactive=20s;
+    open_file_cache_valid 60s;
+    open_file_cache_min_uses 5;
+    open_file_cache_errors off;
+
+    # Compression settings
+    gzip on;
+    gzip_comp_level 6;
+    gzip_min_length 256;
+    gzip_proxied any;
+    gzip_vary on;
+    gzip_types
+        application/javascript
+        application/json
+        application/x-javascript
+        application/xml
+        text/css
+        text/javascript
+        text/plain
+        text/xml
+        text/html
+        application/x-font-ttf
+        font/opentype
+        application/vnd.ms-fontobject
+        image/svg+xml;
 
     resolver 127.0.0.11 ipv6=off;
 
-    #gzip  on;
-
     server {
         listen       80;
         server_name  localhost;
 
-        #charset koi8-r;
-
-        #access_log  logs/host.access.log  main;
-
         location ^~ /api/ {
             proxy_set_header Host $host;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
             proxy_set_header X-Forwarded-Proto $scheme;
+
+            # Proxy timeouts
+            proxy_connect_timeout 60s;
+            proxy_send_timeout 60s;
+            proxy_read_timeout 60s;
+            
+            # Proxy buffering
+            proxy_buffering on;
+            proxy_buffer_size 4k;
+            proxy_buffers 8 16k;
             
             # CORS headers
             add_header 'Access-Control-Allow-Origin' '*' always;
@@ -74,59 +105,23 @@ http {
             try_files $uri $uri/ /index.html;
         }
 
-        location ~* \.(js|css|jpg|png|svg|woff|woff2|ttf|otf|eot|ico)$ {
+
+        # Static asset handling with improved caching
+        location ~* \.(js|css|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|otf|eot)$ {
             root /usr/share/nginx/html;
-            add_header 'Access-Control-Allow-Origin' '*' always;
             expires 30d;
-            add_header Cache-Control "public";
+            add_header Cache-Control "public, no-transform";
+            add_header 'Access-Control-Allow-Origin' '*' always;
+            
+            # Enable compression for these files
+            gzip_static on;  # Serve pre-compressed files if available
+            
+            # Disable access logs for static files
+            access_log off;
         }
-
-        # #error_page  404              /404.html;
-
-        # # redirect server error pages to the static page /50x.html
-        # #
-        # error_page   500 502 503 504  /50x.html;
-        # location = /50x.html {
-        #     root   html;
-        # }
-
-        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
-        #
-        #location ~ \.php$ {
-        #    proxy_pass   http://127.0.0.1;
-        #}
-
-        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
-        #
-        #location ~ \.php$ {
-        #    root           html;
-        #    fastcgi_pass   127.0.0.1:9000;
-        #    fastcgi_index  index.php;
-        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
-        #    include        fastcgi_params;
-        #}
-
-        # deny access to .htaccess files, if Apache's document root
-        # concurs with nginx's one
-        #
-        #location ~ /\.ht {
-        #    deny  all;
-        #}
     }
 
 
-    # another virtual host using mix of IP-, name-, and port-based configuration
-    #
-    #server {
-    #    listen       8000;
-    #    listen       somename:8080;
-    #    server_name  somename  alias  another.alias;
-
-    #    location / {
-    #        root   html;
-    #        index  index.html index.htm;
-    #    }
-    #}
 
 
     # HTTPS server
@@ -149,5 +144,4 @@ http {
     #        index  index.html index.htm;
     #    }
     #}
-
 }
\ No newline at end of file
diff --git a/react-ui/package.json b/react-ui/package.json
index 4c9271009..5767007a1 100755
--- a/react-ui/package.json
+++ b/react-ui/package.json
@@ -13,6 +13,7 @@
         "@fortawesome/free-solid-svg-icons": "^6.6.0",
         "@fortawesome/react-fontawesome": "^0.2.2",
         "@reduxjs/toolkit": "^2.2.4",
+        "@vitejs/plugin-react": "^4.2.1",
         "bootstrap": "^5.3.3",
         "dompurify": "^3.2.3",
         "i18next": "^24.0.5",
@@ -24,11 +25,40 @@
         "react-i18next": "^15.0.0",
         "react-redux": "^9.1.2",
         "react-router-dom": "^6.23.1",
-        "react-scripts": "5.0.1",
         "react-toastify": "^10.0.5",
         "redux": "^5.0.1",
         "redux-observable": "^3.0.0-rc.2",
         "redux-persist": "^6.0.0",
+        "sass": "1.82.0",
+        "sass-embedded": "^1.80.6",
+        "@fullhuman/postcss-purgecss": "^7.0.2",
+        "vite": "^6.0.3"
+    },
+    "devDependencies": {
+        "@babel/runtime": "^7.21.5",
+        "@rtk-query/codegen-openapi": "^2.0.0",
+        "@testing-library/jest-dom": "^6.4.8",
+        "@testing-library/react": "^16.0.0",
+        "@testing-library/user-event": "^14.5.2",
+        "@types/react": "^18.2.66",
+        "@types/react-dom": "^18.2.22",
+        "@typescript-eslint/eslint-plugin": "^8.0.1",
+        "@typescript-eslint/parser": "^8.0.1",
+        "eslint": "^9.9.0",
+        "eslint-config-airbnb-typescript": "^18.0.0",
+        "eslint-config-prettier": "^9.1.0",
+        "eslint-plugin-import": "^2.27.5",
+        "eslint-plugin-jsx-a11y": "^6.7.1",
+        "eslint-plugin-prettier": "^5.2.1",
+        "eslint-plugin-react": "^7.32.2",
+        "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+        "eslint-plugin-react-refresh": "^0.4.9",
+        "globals": "^15.9.0",
+        "prettier": "^3.3.3",
+        "react-scripts": "5.0.1",
+        "typescript": "^5.5.3",
+        "typescript-eslint": "^8.0.1",
+        "vite-bundle-visualizer": "^1.2.1",
         "web-vitals": "^4.2.2"
     },
     "scripts": {
@@ -38,7 +68,8 @@
         "build": "yarn build::api && yarn build::frontend",
         "lint": "eslint src",
         "lint::fix": "eslint src --fix",
-        "dev": "./scripts/dev.sh"
+        "dev": "./scripts/dev.sh",
+        "postbuild": "purgecss --css dist/assets/*.css --content dist/index.html dist/assets/*.js --output dist/purged"
     },
     "eslintConfig": {
         "extends": [
@@ -56,34 +87,5 @@
             "last 1 firefox version",
             "last 1 safari version"
         ]
-    },
-    "devDependencies": {
-        "@babel/runtime": "^7.21.5",
-        "@rtk-query/codegen-openapi": "^2.0.0",
-        "@testing-library/jest-dom": "^6.4.8",
-        "@testing-library/react": "^16.0.0",
-        "@testing-library/user-event": "^14.5.2",
-        "@types/react": "^18.2.66",
-        "@types/react-dom": "^18.2.22",
-        "@typescript-eslint/eslint-plugin": "^8.0.1",
-        "@typescript-eslint/parser": "^8.0.1",
-        "@vitejs/plugin-react": "^4.2.1",
-        "eslint": "^9.9.0",
-        "eslint-config-airbnb-typescript": "^18.0.0",
-        "eslint-config-prettier": "^9.1.0",
-        "eslint-plugin-import": "^2.27.5",
-        "eslint-plugin-jsx-a11y": "^6.7.1",
-        "eslint-plugin-prettier": "^5.2.1",
-        "eslint-plugin-react": "^7.32.2",
-        "eslint-plugin-react-hooks": "^5.1.0-rc.0",
-        "eslint-plugin-react-refresh": "^0.4.9",
-        "globals": "^15.9.0",
-        "prettier": "^3.3.3",
-        "sass": "1.82.0",
-        "sass-embedded": "^1.80.6",
-        "typescript": "^5.5.3",
-        "typescript-eslint": "^8.0.1",
-        "vite": "^6.0.3",
-        "vite-bundle-visualizer": "^1.2.1"
     }
 }
\ No newline at end of file
diff --git a/react-ui/public/fonts/inter-webfont.woff b/react-ui/public/fonts/inter-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..7eca6fe75d3ba56e14981e8f8e1c50d948bb8b56
GIT binary patch
literal 26796
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

literal 0
HcmV?d00001

diff --git a/react-ui/public/fonts/inter-webfont.woff2 b/react-ui/public/fonts/inter-webfont.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..25ef870d53d5616ee3e92bbe3e85b5ed164fe4d2
GIT binary patch
literal 21204
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
Q-fa1H`MqE#XV7g%0OKi>>Hq)$

literal 0
HcmV?d00001

diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx
index 2602463bb..a2c8458a7 100755
--- a/react-ui/src/components/devices/view/device.view.tsx
+++ b/react-ui/src/components/devices/view/device.view.tsx
@@ -6,7 +6,7 @@ import './device.scss';
 import { DeviceViewTable } from './device.view.table';
 import { DeviceViewTabs, DeviceViewTabValues } from './device.view.tabs';
 
-function DeviceView() {
+const DeviceView = () => {
     const { t } = useTranslation('common');
     const searchRef = useRef<HTMLInputElement>(null);
     const { activeTab, setActiveTab, handleActiveTabLink } = useDeviceViewModel();
diff --git a/react-ui/src/components/login/layouts/login.layout.tsx b/react-ui/src/components/login/layouts/login.layout.tsx
index 556dc6de4..58e8fc991 100755
--- a/react-ui/src/components/login/layouts/login.layout.tsx
+++ b/react-ui/src/components/login/layouts/login.layout.tsx
@@ -20,4 +20,6 @@ export const LoginLayout = () => {
     return (
         <LoginView>{outlet}</LoginView>
     )
-}
\ No newline at end of file
+}
+
+export default LoginLayout
\ No newline at end of file
diff --git a/react-ui/src/routes.tsx b/react-ui/src/routes.tsx
index 368df55a4..532d03fbf 100755
--- a/react-ui/src/routes.tsx
+++ b/react-ui/src/routes.tsx
@@ -1,21 +1,43 @@
 import { BasicLayout } from "@layout/basic.layout";
 import { ProtectedLayout } from "@layout/protected.layout/protected.layout";
+import { lazy, Suspense } from 'react';
 import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom";
-import DeviceView from "./components/devices/view/device.view";
-import { LoginLayout } from "./components/login/layouts/login.layout";
 
 export const DEVICE_URL = '/device/';
 export const LOGIN_URL = '/login';
 
+// Lazy load components
+const DeviceView = lazy(() => import('./components/devices/view/device.view'));
+const LoginLayout = lazy(() => import('./components/login/layouts/login.layout'));
+
+// Loading fallback component
+const LoadingFallback = () => <div>Loading...</div>;
 
 export const router = createBrowserRouter(
     createRoutesFromElements(
         <Route element={<BasicLayout />}>
-            <Route path={LOGIN_URL} element={<LoginLayout />} />
+            <Route
+                path={LOGIN_URL}
+                element={
+                    <Suspense fallback={<LoadingFallback />}>
+                        <LoginLayout />
+                    </Suspense>
+                }
+            />
             <Route element={<ProtectedLayout />}>
-                <Route path={DEVICE_URL} element={<DeviceView />} />
-                <Route path="/" element={<Navigate to={DEVICE_URL} replace={true} />} />
+                <Route
+                    path={DEVICE_URL}
+                    element={
+                        <Suspense fallback={<LoadingFallback />}>
+                            <DeviceView />
+                        </Suspense>
+                    }
+                />
+                <Route
+                    path="/"
+                    element={<Navigate to={DEVICE_URL} replace={true} />}
+                />
             </Route>
         </Route>
     )
-)
\ No newline at end of file
+);
\ No newline at end of file
diff --git a/react-ui/src/shared/icons/icons.ts b/react-ui/src/shared/icons/icons.ts
index 00021aa11..9c8791cc9 100755
--- a/react-ui/src/shared/icons/icons.ts
+++ b/react-ui/src/shared/icons/icons.ts
@@ -1,4 +1,4 @@
 import { library } from '@fortawesome/fontawesome-svg-core'
-import { faSpinner, fas } from '@fortawesome/free-solid-svg-icons'
+import { faSpinner } from '@fortawesome/free-solid-svg-icons'
 
-library.add(fas, faSpinner)
\ No newline at end of file
+library.add(faSpinner)
\ No newline at end of file
diff --git a/react-ui/src/shared/style/fonts.scss b/react-ui/src/shared/style/fonts.scss
index c47d1a52f..3af44e155 100755
--- a/react-ui/src/shared/style/fonts.scss
+++ b/react-ui/src/shared/style/fonts.scss
@@ -1,9 +1,12 @@
 @font-face {
     font-family: Inter;
-    src: url("/fonts/Inter.ttf");
+    src:
+        url("/fonts/inter-webfont.woff2") format("woff2"),
+        url("/fonts/inter-webfont.woff") format("woff");
+    font-weight: normal;
+    font-style: normal;
 }
 
-
 * {
     font-family: Inter;
-}
\ No newline at end of file
+}
diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs
index fc4ce4a01..83378d6ee 100755
--- a/react-ui/vite.config.mjs
+++ b/react-ui/vite.config.mjs
@@ -1,13 +1,35 @@
-import react from '@vitejs/plugin-react'
-import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
 
 export default defineConfig({
     plugins: [react()],
     build: {
-        sourcemap: true,
+        sourcemap: false,
+
+        rollupOptions: {
+            output: {
+                manualChunks: {
+                    'required': [
+                        'bootstrap', 'react-bootstrap',
+                        'react', 'react-dom', 'react-router-dom',
+                        'redux', '@reduxjs/toolkit', 'react-redux', 'redux-observable', 'redux-persist',
+                        'i18next', 'react-i18next',
+                        '@fortawesome/fontawesome-svg-core',
+                        '@fortawesome/free-regular-svg-icons',
+                        '@fortawesome/free-solid-svg-icons',
+                        '@fortawesome/react-fontawesome'
+                    ],
+                    'lazy': [
+                        'react-toastify'
+                    ]
+                }
+            }
+        }
     },
     // develop server
     server: {
+        sourcemap: true,
         host: true,
         port: 3000,
         proxy: {
diff --git a/react-ui/yarn.lock b/react-ui/yarn.lock
index d054db6d6..62e8d8fa1 100755
--- a/react-ui/yarn.lock
+++ b/react-ui/yarn.lock
@@ -1486,6 +1486,13 @@
   dependencies:
     prop-types "^15.8.1"
 
+"@fullhuman/postcss-purgecss@^7.0.2":
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-7.0.2.tgz#ccacdbc312248c76c42cfac359f4ca5121001e67"
+  integrity sha512-U4zAXNaVztbDxO9EdcLp51F3UxxYsb/7DN89rFxFJhfk2Wua2pvw2Kf3HdspbPhW/wpHjSjsxWYoIlbTgRSjbQ==
+  dependencies:
+    purgecss "^7.0.2"
+
 "@humanfs/core@^0.19.1":
   version "0.19.1"
   resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
@@ -3906,6 +3913,11 @@ combined-stream@^1.0.8:
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@^12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
+  integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -5690,6 +5702,18 @@ glob@^10.3.10:
     package-json-from-dist "^1.0.0"
     path-scurry "^1.11.1"
 
+glob@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e"
+  integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==
+  dependencies:
+    foreground-child "^3.1.0"
+    jackspeak "^4.0.1"
+    minimatch "^10.0.0"
+    minipass "^7.1.2"
+    package-json-from-dist "^1.0.0"
+    path-scurry "^2.0.0"
+
 glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -6469,6 +6493,13 @@ jackspeak@^3.1.2:
   optionalDependencies:
     "@pkgjs/parseargs" "^0.11.0"
 
+jackspeak@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015"
+  integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==
+  dependencies:
+    "@isaacs/cliui" "^8.0.2"
+
 jake@^10.8.5:
   version "10.9.2"
   resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f"
@@ -7277,6 +7308,11 @@ lru-cache@^10.2.0:
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
   integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
 
+lru-cache@^11.0.0:
+  version "11.0.2"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39"
+  integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==
+
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -7407,6 +7443,13 @@ minimalistic-assert@^1.0.0:
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
   integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
 
+minimatch@^10.0.0:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b"
+  integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -7923,6 +7966,14 @@ path-scurry@^1.11.1:
     lru-cache "^10.2.0"
     minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
 
+path-scurry@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580"
+  integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==
+  dependencies:
+    lru-cache "^11.0.0"
+    minipass "^7.1.2"
+
 path-to-regexp@0.1.12:
   version "0.1.12"
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
@@ -8653,6 +8704,16 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
   integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
 
+purgecss@^7.0.2:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-7.0.2.tgz#b7dccc3ead65a4301eed98e014793719a511c633"
+  integrity sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ==
+  dependencies:
+    commander "^12.1.0"
+    glob "^11.0.0"
+    postcss "^8.4.47"
+    postcss-selector-parser "^6.1.2"
+
 q@^1.1.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
-- 
GitLab


From 431c22efbbae886e1cc9a068e0360e45b3882083 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Thu, 9 Jan 2025 08:07:16 +0000
Subject: [PATCH 23/45] [renovate] Update github.com/aristanetworks/goarista
 digest to 362a04c

See merge request danet/gosdn!1149

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 1946630f6..d7343eca0 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module code.fbi.h-da.de/danet/gosdn
 go 1.23
 
 require (
-	github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44
+	github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029
 	github.com/c-bata/go-prompt v0.2.6
 	github.com/docker/docker v24.0.9+incompatible
 	github.com/google/go-cmp v0.6.0
diff --git a/go.sum b/go.sum
index fe429b64a..83ec9b6c1 100644
--- a/go.sum
+++ b/go.sum
@@ -60,6 +60,8 @@ github.com/aristanetworks/goarista v0.0.0-20241101122619-a6d58bf1ed81 h1:CpeoPCo
 github.com/aristanetworks/goarista v0.0.0-20241101122619-a6d58bf1ed81/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
 github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44 h1:vb3HPPa1CegMZY90JF8mDyxXiV+qJJuSWwMhBZCcsws=
 github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
+github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029 h1:bvw2TILeXtuYfZ9rip/4DY933UuIvCwtvJmwvz978ac=
+github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
 github.com/aristanetworks/gomap v0.0.0-20240724180630-b4cffb90720f h1:3GwV1IeLp0PwWcnbc9ZihE3osvexJf3PMjWSCGjtIqc=
 github.com/aristanetworks/gomap v0.0.0-20240724180630-b4cffb90720f/go.mod h1:bNzH6HFWav8D/ws3QlkjLpf9ZOdsUTDx+qJikWCcGRc=
 github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
-- 
GitLab


From 2d6e31d405b6f326db5a0f2cae53b9181350fcaa Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Thu, 9 Jan 2025 08:16:26 +0000
Subject: [PATCH 24/45] [renovate] Update module golang.org/x/crypto to v0.32.0

See merge request danet/gosdn!1150

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 6 +++---
 go.sum | 6 ++++++
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index d7343eca0..7c8225cdc 100644
--- a/go.mod
+++ b/go.mod
@@ -77,10 +77,10 @@ require (
 	github.com/xdg-go/stringprep v1.0.4 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
-	golang.org/x/crypto v0.31.0
+	golang.org/x/crypto v0.32.0
 	golang.org/x/net v0.33.0
-	golang.org/x/sys v0.28.0 // indirect
-	golang.org/x/term v0.27.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
+	golang.org/x/term v0.28.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 83ec9b6c1..eb454ef9b 100644
--- a/go.sum
+++ b/go.sum
@@ -423,6 +423,8 @@ golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
 golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
 golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
@@ -517,6 +519,8 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
 golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
 golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -530,6 +534,8 @@ golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
 golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
 golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
 golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-- 
GitLab


From 7dfa8398f470c6e53934c5c07298f20b6ff9da2f Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Thu, 9 Jan 2025 13:17:49 +0100
Subject: [PATCH 25/45] (ui): reduced complexity of rehydration process

---
 .../devices/reducer/device.reducer.ts         | 58 +++++++++++++++----
 .../devices/routines/device.routine.ts        |  2 +-
 .../devices/routines/mne.routine.ts           | 12 ++--
 .../view_model/device.table.viewmodel.ts      |  2 +-
 react-ui/src/index.tsx                        |  8 ---
 .../src/shared/reducer/routine.reducer.ts     | 35 +++--------
 react-ui/src/shared/types/thunk.type.ts       | 15 +----
 .../shared/utils/routine-holder.singleton.ts  | 47 ---------------
 8 files changed, 65 insertions(+), 114 deletions(-)
 delete mode 100644 react-ui/src/shared/utils/routine-holder.singleton.ts

diff --git a/react-ui/src/components/devices/reducer/device.reducer.ts b/react-ui/src/components/devices/reducer/device.reducer.ts
index 4afcba788..cea12fbc9 100755
--- a/react-ui/src/components/devices/reducer/device.reducer.ts
+++ b/react-ui/src/components/devices/reducer/device.reducer.ts
@@ -5,6 +5,7 @@ import {
 } from '@api/api'
 import { DeviceViewTabValues } from '@component/devices/view/device.view.tabs'
 import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { REHYDRATE } from 'redux-persist'
 import { RootState } from 'src/stores'
 import '../routines/index'
 import { startListening } from '/src/stores/middleware/listener.middleware'
@@ -32,6 +33,13 @@ const initialState: DeviceSliceState = {
     selected: null,
 }
 
+interface SetSelectedDeviceType {
+    device: Device | null,
+    options?: {
+        bypassCheck: boolean
+    }
+}
+
 const deviceSlice = createSlice({
     name: 'device',
     initialState,
@@ -46,24 +54,33 @@ const deviceSlice = createSlice({
             state.activeTab = action.payload
         },
         setSelectedDevice: {
-            reducer: (state, action: PayloadAction<Device | null, string, { skipListener?: boolean }>) => {
-                // do thing if desired device is already selected
-                if (state.selected?.device.id === action.payload?.id) {
-                    action.meta.skipListener = true
+            reducer: (state, { payload, meta }: PayloadAction<SetSelectedDeviceType, string, { skipListener?: boolean }>) => {
+                /**
+                 * Do nothing if desired device is already selected
+                 * Bypass the check if the flag is set to true. We
+                 * use this bypass to trigger the listener functions
+                 * accordingly
+                 */
+                if (!payload?.options?.bypassCheck && state.selected?.device.id === payload.device?.id) {
+                    meta.skipListener = true
                     return
                 }
 
-                let selectedObject = null;
-                if (action.payload) {
-                    selectedObject = { device: action.payload, mne: null, json: null }
+                if (!payload.device) {
+                    throw Error('Passed null device as parameter while bypassing the safety check')
+                }
+
+                let selectedObject: SelectedObject | null = null;
+                if (payload) {
+                    selectedObject = { device: payload.device, mne: null, json: null }
                 }
 
                 state.selected = selectedObject
             },
-            prepare: (device: Device | null) => {
+            prepare: (payload) => {
                 return {
-                    payload: device,
-                    meta: { skipListener: false } // set to true when needed
+                    payload,
+                    meta: { skipListener: false }
                 }
             }
         },
@@ -107,7 +124,24 @@ startListening({
         }
 
         // if there are no devices available do set null
-        const newDevices = action.payload?.[0] || null
-        listenerApi.dispatch(setSelectedDevice(newDevices))
+        const device = action.payload?.[0] || null
+        listenerApi.dispatch(setSelectedDevice({ device } as SetSelectedDeviceType))
     },
 })
+
+
+/**
+ * On startup reset the selected device 
+ */
+startListening({
+    predicate: ({ type }: any) => type === REHYDRATE,
+    effect: async (_, listenerApi) => {
+        const { device: state } = listenerApi.getState() as RootState
+        const device = state.selected?.device
+        if (!device) {
+            return
+        }
+
+        listenerApi.dispatch(setSelectedDevice({ device, options: { bypassCheck: true } } as SetSelectedDeviceType))
+    },
+})
\ No newline at end of file
diff --git a/react-ui/src/components/devices/routines/device.routine.ts b/react-ui/src/components/devices/routines/device.routine.ts
index f9b3e1ee2..ef92b1c8e 100755
--- a/react-ui/src/components/devices/routines/device.routine.ts
+++ b/react-ui/src/components/devices/routines/device.routine.ts
@@ -20,7 +20,7 @@ export const fetchDevicesThunk = createAsyncThunk(FETCH_DEVICE_ACTION, (_, thunk
     const { user } = thunkApi.getState() as RootState
 
     if (!user.user?.roles) {
-        throw new Error('Background MNE fetching failed! User data is missing. Reload page or logout and login again')
+        throw new Error('Background device fetching failed! User data is missing. Reload page or logout and login again')
         // TODO
     }
 
diff --git a/react-ui/src/components/devices/routines/mne.routine.ts b/react-ui/src/components/devices/routines/mne.routine.ts
index 876317dd2..292869307 100755
--- a/react-ui/src/components/devices/routines/mne.routine.ts
+++ b/react-ui/src/components/devices/routines/mne.routine.ts
@@ -8,12 +8,12 @@ import {
 import { createAsyncThunk } from '@reduxjs/toolkit'
 import { addRoutine } from '@shared/reducer/routine.reducer'
 import { Category, CategoryType } from '@shared/types/category.type'
-import { RoutineHolderSingleton } from '@utils/routine-holder.singleton'
 import { RootState } from 'src/stores'
 import { startListening } from '../../../stores/middleware/listener.middleware'
 
 export const FETCH_MNE_ACTION = 'subscription/device/fetchSelectedMNE'
 
+
 /**
  * #0
  * Trigger fetch MNE (#1)
@@ -21,14 +21,16 @@ export const FETCH_MNE_ACTION = 'subscription/device/fetchSelectedMNE'
  * Triggered by a selectedDevice
  */
 startListening({
-    predicate: (action) => setSelectedDevice.match(action) && !!action.payload && !action.meta?.skipListener,
+    predicate: (action) => setSelectedDevice.match(action) && !!action.payload.device && !action.meta?.skipListener,
     effect: async (action, listenerApi) => {
-        const factory = RoutineHolderSingleton.getInstance();
+
+        const device = action.payload.device
+
         listenerApi.dispatch(
             addRoutine({
-                thunk: factory.getRoutineByName("fetchSelectedMneThunk"),
+                thunk: fetchSelectedMneThunk,
                 category: Category.TAB as CategoryType,
-                payload: action.payload as Object,
+                payload: device,
             })
         )
     },
diff --git a/react-ui/src/components/devices/view_model/device.table.viewmodel.ts b/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
index df7595328..5769780ff 100755
--- a/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
@@ -26,7 +26,7 @@ export const useDeviceTableViewModel = (searchRef) => {
     }, []);
 
     const trClickHandler = (device: Device) => {
-        dispatch(setSelectedDevice(device));
+        dispatch(setSelectedDevice({ device }));
     }
 
 
diff --git a/react-ui/src/index.tsx b/react-ui/src/index.tsx
index 559e1bc54..3697efd07 100755
--- a/react-ui/src/index.tsx
+++ b/react-ui/src/index.tsx
@@ -1,6 +1,4 @@
-import { fetchSelectedMneThunk } from '@component/devices/routines/mne.routine'
 import { UtilsProvider } from '@provider/utils.provider'
-import { RoutineHolderSingleton } from '@utils/routine-holder.singleton'
 import i18next from 'i18next'
 import React from 'react'
 import ReactDOM from 'react-dom/client'
@@ -20,12 +18,6 @@ import { persistor, store } from './stores'
 
 window.env = window.location.hostname === 'localhost' ? 'development' : 'production';
 
-const factory = RoutineHolderSingleton.getInstance();
-factory.registerRoutine("fetchSelectedMneThunk", {
-    func: fetchSelectedMneThunk,
-    id: 0
-});
-
 ReactDOM.createRoot(document.getElementById("root")).render(
     <React.StrictMode>
         <ErrorBoundary fallback={<div>Something went wrong</div>}>
diff --git a/react-ui/src/shared/reducer/routine.reducer.ts b/react-ui/src/shared/reducer/routine.reducer.ts
index f8c8c31eb..5e9c3401a 100755
--- a/react-ui/src/shared/reducer/routine.reducer.ts
+++ b/react-ui/src/shared/reducer/routine.reducer.ts
@@ -1,9 +1,7 @@
 import { PayloadAction, createSlice } from '@reduxjs/toolkit'
 import { CategoryType } from '@shared/types/category.type'
 import { ThunkDTO, ThunkPersist } from '@shared/types/thunk.type'
-import { RoutineHolderSingleton } from '@utils/routine-holder.singleton'
 import { RoutineManager } from '@utils/routine.manager'
-import { REHYDRATE } from 'redux-persist'
 import { RootState } from '../../stores'
 import { startListening } from '../../stores/middleware/listener.middleware'
 import { setToken } from './user.reducer'
@@ -52,30 +50,6 @@ startListening({
     },
 })
 
-// on rehydrate add all persistet routines
-// TODO -> thunk does not have the thunk function object due to its coming from the store that ignores the value.
-// at this point we have to figure out how to get the thunk function out of the "string" name
-startListening({
-    predicate: ({ type }) => type === REHYDRATE,
-    effect: async (_, listenerApi) => {
-        const { routine } = listenerApi.getState() as RootState
-        const routines = RoutineHolderSingleton.getInstance()
-
-        Object.values(routine.thunks)
-            .filter(thunk => !!thunk)
-            .forEach(thunk => {
-                const container = routines.getRoutineById(thunk.thunkId)
-                const dto: ThunkDTO = {
-                    category: thunk.category,
-                    payload: thunk.payload,
-                    thunk: container
-                }
-
-                listenerApi.dispatch(addRoutine(dto))
-            })
-    },
-})
-
 /**
  * Add new routine
  * 
@@ -87,12 +61,17 @@ startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
         const { thunk } = action.payload as ThunkDTO
-        const subscription = await listenerApi.dispatch(thunk.func(action.payload.payload))
+        const subscription = await listenerApi.dispatch(thunk(action.payload.payload))
+
+        if (subscription.error) {
+            throw new Error('Error during routine execution: ' + subscription.error.message)
+        }
+
         RoutineManager.add(subscription.payload, action.payload.category)
     },
 })
 
-// unsubscribe old routine
+// unsubscribe old routine that is in the same category
 startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
diff --git a/react-ui/src/shared/types/thunk.type.ts b/react-ui/src/shared/types/thunk.type.ts
index 90b846039..9143871f0 100644
--- a/react-ui/src/shared/types/thunk.type.ts
+++ b/react-ui/src/shared/types/thunk.type.ts
@@ -1,21 +1,12 @@
 import { CategoryType } from "./category.type"
 
 
-/**
- * Contains the thunk function combined with a unique id
- * Giving a explicit id (and not the index of the object)
- * prevents missmatching the function if a update changes
- * the RoutineList object length 
- */
-export interface ThunkContainer {
-    id: number
-    func: any,
-}
+// The actual Thunk type is hard to determine because is very generic 
+export type ThunkFunc = any
 
 
 export interface ThunkDTO {
-    thunk: ThunkContainer
-
+    thunk: ThunkFunc
     payload: Object
 
     /**
diff --git a/react-ui/src/shared/utils/routine-holder.singleton.ts b/react-ui/src/shared/utils/routine-holder.singleton.ts
deleted file mode 100644
index 47332ab0b..000000000
--- a/react-ui/src/shared/utils/routine-holder.singleton.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { ThunkContainer } from "@shared/types/thunk.type";
-
-interface LocalThunkContainer {
-    container: ThunkContainer,
-    name: string
-}
-
-export class RoutineHolderSingleton {
-    private static instance: RoutineHolderSingleton;
-    private routineList: Array<LocalThunkContainer> = []
-
-    private constructor() { }
-
-    static getInstance(): RoutineHolderSingleton {
-        if (!RoutineHolderSingleton.instance) {
-            RoutineHolderSingleton.instance = new RoutineHolderSingleton();
-        }
-        return RoutineHolderSingleton.instance;
-    }
-
-    registerRoutine(name: string, thunk: ThunkContainer) {
-        this.routineList = [...this.routineList, { container: thunk, name }];
-    }
-
-    getRoutineById(id: number): ThunkContainer {
-        const routine = this.routineList.find((thunk) => thunk.container.id === id)
-
-        if (!routine) {
-            throw new Error('')
-            // TODO
-        }
-
-        return routine.container;
-    }
-
-
-    getRoutineByName(name: string): ThunkContainer {
-        const routine = this.routineList.find((thunk) => thunk.name === name)
-
-        if (!routine) {
-            throw new Error('')
-            // TODO
-        }
-
-        return routine.container;
-    }
-}
\ No newline at end of file
-- 
GitLab


From eed9fbb5d2b2236a62d6dd0acf0a159c8310b35d Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Thu, 9 Jan 2025 14:02:32 +0100
Subject: [PATCH 26/45] (ui): add highlight on search in table

---
 .../src/components/devices/api/pnd.fetch.ts   | 15 ----------
 .../devices/view/device.view.table.tsx        | 30 ++++++++++++-------
 .../json_viewer/view/json_viewer.view.tsx     |  8 +----
 react-ui/src/shared/helper/text.ts            | 12 ++++++++
 .../protected.layout/protected.layout.tsx     |  3 +-
 .../user.fetch.ts => routine/user.routine.ts} | 14 ++++++++-
 6 files changed, 46 insertions(+), 36 deletions(-)
 delete mode 100644 react-ui/src/components/devices/api/pnd.fetch.ts
 create mode 100644 react-ui/src/shared/helper/text.ts
 rename react-ui/src/shared/{api/user.fetch.ts => routine/user.routine.ts} (63%)

diff --git a/react-ui/src/components/devices/api/pnd.fetch.ts b/react-ui/src/components/devices/api/pnd.fetch.ts
deleted file mode 100644
index fd49de636..000000000
--- a/react-ui/src/components/devices/api/pnd.fetch.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { PndServiceGetPndListApiArg, api } from "@api/api"
-import { createAsyncThunk } from "@reduxjs/toolkit"
-import { setPnds } from "../reducer/device.reducer"
-
-// TODO rethink this. This should be in the shared part bc its getting invoked in the procteded layout
-export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
-    const payload: PndServiceGetPndListApiArg = {
-        timestamp: new Date().getTime().toString(),
-    }
-
-    const subscription = thunkApi.dispatch(api.endpoints.pndServiceGetPndList.initiate(payload))
-    subscription.unwrap().then((response) => {
-        thunkApi.dispatch(setPnds(response.pnd))
-    })
-})
\ No newline at end of file
diff --git a/react-ui/src/components/devices/view/device.view.table.tsx b/react-ui/src/components/devices/view/device.view.table.tsx
index 9b731fef5..337148f92 100755
--- a/react-ui/src/components/devices/view/device.view.table.tsx
+++ b/react-ui/src/components/devices/view/device.view.table.tsx
@@ -1,4 +1,6 @@
+import { insertMarkTags } from "@helper/text";
 import { useAppSelector } from "@hooks";
+import DOMPurify from 'dompurify';
 import { MutableRefObject, useCallback } from "react";
 import { OverlayTrigger, Table, Tooltip } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
@@ -15,25 +17,30 @@ export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) =
     }
 
     const getDeviceTable = useCallback(() => {
-        return devices.filter((device) => {
-            if (!searchRef.current?.value) {
-                return true;
-            }
+        const search = searchRef.current?.value;
+        let filtered = devices
 
-            const searchInput = searchRef.current!.value;
-            const user = pnds.find(pnd => pnd.id === device.pid);
+        // filter if something is in search
+        if (search) {
+            filtered = devices.filter((device) => {
+                const user = pnds.find(pnd => pnd.id === device.pid);
+
+                return device.id?.includes(search) ||
+                    device.name?.includes(search) ||
+                    user?.name?.includes(search);
+            })
+        }
 
-            return device.id.includes(searchInput) || device.name.includes(searchInput) || user?.name.includes(searchInput);
-        }).map((device, index) => {
+        return filtered.map((device, index) => {
             const user = pnds.find(pnd => pnd.id === device.pid);
 
             return (
                 <tr key={index} onClick={() => trClickHandler(device)} className={selectedDevice?.device.id === device.id ? 'active' : ''}>
-                    <td>{device.name}</td>
+                    <td key={0} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(device.name!, search) : DOMPurify.sanitize(device.name) }}></td>
                     <OverlayTrigger overlay={<Tooltip id={device.id}>{device.id}</Tooltip>}>
-                        <td>{cropUUID(device.id)}</td>
+                        <td dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(cropUUID(device.id!), search) : DOMPurify.sanitize(cropUUID(device.id!)) }}></td>
                     </OverlayTrigger>
-                    <td>{user?.name || ''}</td>
+                    <td key={1} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(user?.name || '', search) : DOMPurify.sanitize(user?.name || '') }}></td>
                     <td></td>
                 </tr>
             )
@@ -41,6 +48,7 @@ export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) =
     }, [devices, searchRef, pnds, selectedDevice, trClickHandler]);
 
 
+
     return (
         <Table striped responsive className="device-table">
             <thead>
diff --git a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
index b8358c686..33e52a397 100755
--- a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
+++ b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
@@ -1,5 +1,6 @@
 import { faAlignRight, faPenToSquare, faTrashCan } from "@fortawesome/free-solid-svg-icons"
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import { insertMarkTags } from "@helper/text"
 import DOMPurify from 'dompurify'
 import React, { Suspense, useMemo, useRef } from "react"
 import { Form, Table } from "react-bootstrap"
@@ -30,13 +31,6 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
         )
     }, [breadcrumbs])
 
-    const insertMarkTags = (text: string, search: string): string => {
-        const start = text.indexOf(search)
-        const end = start + search.length
-
-        return DOMPurify.sanitize(text.substring(0, start)) + "<span class='highlight'>" + DOMPurify.sanitize(search) + "</span>" + DOMPurify.sanitize(text.substring(end, text.length))
-    }
-
     const renderInner = (innerJson: JSON, nested: number = 0, parentKey: string = "", path: string = "/network-instance/0/"): JSX.Element => {
         path += parentKey + (parentKey === "" ? "" : "/")
 
diff --git a/react-ui/src/shared/helper/text.ts b/react-ui/src/shared/helper/text.ts
new file mode 100644
index 000000000..6aee13790
--- /dev/null
+++ b/react-ui/src/shared/helper/text.ts
@@ -0,0 +1,12 @@
+
+import DOMPurify from 'dompurify'
+
+export const insertMarkTags = (text: string, search: string): string => {
+    const start = text.indexOf(search)
+    if (start === -1) {
+        return DOMPurify.sanitize(text)
+    }
+    const end = start + search.length
+
+    return DOMPurify.sanitize(text.substring(0, start)) + "<span class='highlight'>" + DOMPurify.sanitize(search) + "</span>" + DOMPurify.sanitize(text.substring(end, text.length))
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
index 17b6209a5..b017cf56f 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
@@ -1,12 +1,11 @@
-import { fetchUser } from '@api/user.fetch';
 import logo from '@assets/logo.svg';
-import { fetchPnds } from '@component/devices/api/pnd.fetch';
 import { faCircleUser, faRightFromBracket } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { useAppDispatch, useAppSelector } from '@hooks';
 import { useAuth } from "@provider/auth.provider";
 import { MenuProvider } from '@provider/menu/menu.provider';
 import { DEVICE_URL, LOGIN_URL } from '@routes';
+import { fetchPnds, fetchUser } from '@shared/routine/user.routine';
 import React, { useEffect } from "react";
 import { Dropdown } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
diff --git a/react-ui/src/shared/api/user.fetch.ts b/react-ui/src/shared/routine/user.routine.ts
similarity index 63%
rename from react-ui/src/shared/api/user.fetch.ts
rename to react-ui/src/shared/routine/user.routine.ts
index 08806783b..368ccf412 100644
--- a/react-ui/src/shared/api/user.fetch.ts
+++ b/react-ui/src/shared/routine/user.routine.ts
@@ -1,7 +1,8 @@
+import { api, PndServiceGetPndListApiArg, UserServiceGetUsersApiArg } from "@api/api"
+import { setPnds } from "@component/devices/reducer/device.reducer"
 import { createAsyncThunk } from "@reduxjs/toolkit"
 import { setUser } from "@shared/reducer/user.reducer"
 import { RootState } from "src/stores"
-import { api, UserServiceGetUsersApiArg } from "./api"
 
 export const fetchUser = createAsyncThunk('user/fetchUser', (_, thunkAPI) => {
     const payload: UserServiceGetUsersApiArg = {}
@@ -23,3 +24,14 @@ export const fetchUser = createAsyncThunk('user/fetchUser', (_, thunkAPI) => {
         thunkAPI.dispatch(setUser(matchedUser))
     })
 })
+
+export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
+    const payload: PndServiceGetPndListApiArg = {
+        timestamp: new Date().getTime().toString(),
+    }
+
+    const subscription = thunkApi.dispatch(api.endpoints.pndServiceGetPndList.initiate(payload))
+    subscription.unwrap().then((response) => {
+        thunkApi.dispatch(setPnds(response.pnd))
+    })
+})
\ No newline at end of file
-- 
GitLab


From 4585c77df111eca5d2430c146ab3d084be5b1e71 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Thu, 9 Jan 2025 18:35:57 +0100
Subject: [PATCH 27/45] (ui): add copy and copy row menu options

---
 react-ui/package.json                         |  5 +-
 .../devices/view/device.view.table.tsx        | 35 ++++----
 .../view_model/device.table.viewmodel.ts      | 84 +++++++++++++++++--
 .../src/i18n/locales/en/translations.json     |  7 +-
 .../viewmodel/json_viewer.viewmodel.tsx       | 45 +++++-----
 .../shared/provider/menu/menu.provider.tsx    | 43 ++++++----
 react-ui/yarn.lock                            |  5 ++
 7 files changed, 162 insertions(+), 62 deletions(-)

diff --git a/react-ui/package.json b/react-ui/package.json
index 5767007a1..30db7c97c 100755
--- a/react-ui/package.json
+++ b/react-ui/package.json
@@ -12,9 +12,11 @@
         "@fortawesome/free-regular-svg-icons": "^6.6.0",
         "@fortawesome/free-solid-svg-icons": "^6.6.0",
         "@fortawesome/react-fontawesome": "^0.2.2",
+        "@fullhuman/postcss-purgecss": "^7.0.2",
         "@reduxjs/toolkit": "^2.2.4",
         "@vitejs/plugin-react": "^4.2.1",
         "bootstrap": "^5.3.3",
+        "crypto-js": "^4.2.0",
         "dompurify": "^3.2.3",
         "i18next": "^24.0.5",
         "jwt-decode": "^4.0.0",
@@ -31,7 +33,6 @@
         "redux-persist": "^6.0.0",
         "sass": "1.82.0",
         "sass-embedded": "^1.80.6",
-        "@fullhuman/postcss-purgecss": "^7.0.2",
         "vite": "^6.0.3"
     },
     "devDependencies": {
@@ -88,4 +89,4 @@
             "last 1 safari version"
         ]
     }
-}
\ No newline at end of file
+}
diff --git a/react-ui/src/components/devices/view/device.view.table.tsx b/react-ui/src/components/devices/view/device.view.table.tsx
index 337148f92..56ad05f3f 100755
--- a/react-ui/src/components/devices/view/device.view.table.tsx
+++ b/react-ui/src/components/devices/view/device.view.table.tsx
@@ -1,20 +1,20 @@
 import { insertMarkTags } from "@helper/text";
 import { useAppSelector } from "@hooks";
 import DOMPurify from 'dompurify';
-import { MutableRefObject, useCallback } from "react";
+import { MutableRefObject, useCallback, useRef } from "react";
 import { OverlayTrigger, Table, Tooltip } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 import { useDeviceTableViewModel } from "../view_model/device.table.viewmodel";
 
+const cropUUID = (uuid: string): string => {
+    return uuid.substring(0, 3) + "..." + uuid.substring(uuid.length - 3, uuid.length);
+}
+
 export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) => {
     const { devices, pnds, selected: selectedDevice } = useAppSelector(state => state.device);
     const { t } = useTranslation('common');
-    const { trClickHandler } = useDeviceTableViewModel(searchRef);
-
-
-    const cropUUID = (uuid: string): string => {
-        return uuid.substring(0, 3) + "..." + uuid.substring(uuid.length - 3, uuid.length);
-    }
+    const tableRef = useRef();
+    const { trClickHandler } = useDeviceTableViewModel(searchRef, tableRef);
 
     const getDeviceTable = useCallback(() => {
         const search = searchRef.current?.value;
@@ -34,23 +34,28 @@ export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) =
         return filtered.map((device, index) => {
             const user = pnds.find(pnd => pnd.id === device.pid);
 
+            const username = user?.name || ''
+            const deviceId = device.id!;
+            const cropedId = cropUUID(deviceId)
+            const devicename = device.name || '';
+
+            const rowData = username + ";" + deviceId + ";" + devicename
+
             return (
-                <tr key={index} onClick={() => trClickHandler(device)} className={selectedDevice?.device.id === device.id ? 'active' : ''}>
-                    <td key={0} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(device.name!, search) : DOMPurify.sanitize(device.name) }}></td>
-                    <OverlayTrigger overlay={<Tooltip id={device.id}>{device.id}</Tooltip>}>
-                        <td dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(cropUUID(device.id!), search) : DOMPurify.sanitize(cropUUID(device.id!)) }}></td>
+                <tr data-copy-value={rowData} key={index} onClick={() => trClickHandler(device)} className={selectedDevice?.device.id === deviceId ? 'active' : ''}>
+                    <td data-copy-value={devicename} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(devicename, search) : DOMPurify.sanitize(devicename) }}></td>
+                    <OverlayTrigger overlay={<Tooltip id={device.id}>{deviceId}</Tooltip>}>
+                        <td data-copy-value={deviceId} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(cropedId, search) : DOMPurify.sanitize(cropedId) }}></td>
                     </OverlayTrigger>
-                    <td key={1} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(user?.name || '', search) : DOMPurify.sanitize(user?.name || '') }}></td>
+                    <td data-copy-value={username} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(username, search) : DOMPurify.sanitize(username) }}></td>
                     <td></td>
                 </tr>
             )
         })
     }, [devices, searchRef, pnds, selectedDevice, trClickHandler]);
 
-
-
     return (
-        <Table striped responsive className="device-table">
+        <Table striped responsive className="device-table" ref={tableRef}>
             <thead>
                 <tr>
                     <th>{t('device.table.header.name')}</th>
diff --git a/react-ui/src/components/devices/view_model/device.table.viewmodel.ts b/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
index 5769780ff..4b328d639 100755
--- a/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
@@ -1,29 +1,97 @@
 import { Device, setSelectedDevice } from "@component/devices/reducer/device.reducer";
+import { faCopy } from "@fortawesome/free-solid-svg-icons";
 import { useAppDispatch } from "@hooks";
+import { useMenu } from "@provider/menu/menu.provider";
+import { useUtils } from "@provider/utils.provider";
 import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { toast } from "react-toastify";
 
-export const useDeviceTableViewModel = (searchRef) => {
+export const useDeviceTableViewModel = (searchRef, tableRef) => {
     const [searchTerm, setSearchTerm] = useState('');
     const dispatch = useAppDispatch();
+    const { subscribe } = useMenu();
+    const { toClipboard } = useUtils();
+    const { t } = useTranslation('common');
+
+
+    const registerMenuOptions = () => {
+        const subscription = subscribe({
+            target: tableRef.current,
+            actions: [
+                {
+                    key: t('device.table.actions.copy'),
+                    icon: faCopy,
+                    action: (clickedElement) => {
+                        if (clickedElement) {
+                            const text = clickedElement.dataset.copyValue
+                            if (!text) {
+                                toast.warn(t('global.toast.copied_failed'))
+                                return
+                            }
+
+
+                            toClipboard(text)
+                        }
+                    }
+                },
+
+                {
+                    key: t('device.table.actions.copy_row'),
+                    icon: faCopy,
+                    action: (clickedElement) => {
+                        let parent = clickedElement;
+                        while (parent && parent.tagName !== 'TR') {
+                            parent = parent.parentNode;
+                        }
+
+                        const text = parent.dataset.copyValue
+                        if (!text) {
+                            toast.warn(t('global.toast.copied_failed'))
+                            return
+                        }
+                        toClipboard(text)
+                    }
+                }
+            ]
+        })
+
+        return () => {
+            subscription.unsubscribe()
+        }
+    }
+
+    // seperate use effect to rerun this after tableref and subscribe are initialized
+    useEffect(() => {
+        if (!subscribe || !tableRef.current) {
+            return
+        }
+
+        const unsubscribe = registerMenuOptions()
+
+        return () => {
+            unsubscribe()
+        }
+    }, [tableRef, subscribe])
 
 
     useEffect(() => {
+        if (!searchRef.current) {
+            return
+        }
+
         const handleSearchChange = () => {
-            if (searchRef.current) {
-                setSearchTerm(searchRef.current.value);
-            }
+            setSearchTerm(searchRef.current.value);
         };
 
-        if (searchRef.current) {
-            searchRef.current.addEventListener('input', handleSearchChange);
-        }
+        searchRef.current.addEventListener('input', handleSearchChange);
 
         return () => {
             if (searchRef.current) {
                 searchRef.current.removeEventListener('input', handleSearchChange);
             }
         };
-    }, []);
+    }, [searchRef]);
 
     const trClickHandler = (device: Device) => {
         dispatch(setSelectedDevice({ device }));
diff --git a/react-ui/src/i18n/locales/en/translations.json b/react-ui/src/i18n/locales/en/translations.json
index 46b76563c..fb3ca729c 100755
--- a/react-ui/src/i18n/locales/en/translations.json
+++ b/react-ui/src/i18n/locales/en/translations.json
@@ -6,7 +6,8 @@
                 "empty_field": "This field can´t be empty"
             },
             "toast": {
-                "copied": "Copied to clipboard"
+                "copied": "Copied to clipboard",
+                "copied_failed": "Copying to clipboard failed"
             },
             "menu_item": {
                 "logout": "Logout"
@@ -35,6 +36,10 @@
                     "uuid": "UUID",
                     "user": "User",
                     "last_updated": "Last updated"
+                },
+                "actions": {
+                    "copy": "Copy",
+                    "copy_row": "Copy row"
                 }
             },
             "search": {
diff --git a/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx b/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx
index 6c3ac78f3..f0bc92212 100644
--- a/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx
+++ b/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx
@@ -67,29 +67,31 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy
     }
 
     const registerMenuOptions = () => {
-        if (container.current) {
-            const subscription = subscribe({
-                target: container.current,
-                actions: [
-                    {
-                        key: t('json_viewer.copy'),
-                        icon: faCopy,
-                        action: (clickedElement) => {
-                            let parent = clickedElement;
-                            while (parent && parent.tagName !== 'TR') {
-                                parent = parent.parentNode;
-                            }
-
-                            const copyValue = parent.dataset.copyValue
-                            toClipboard(copyValue)
+        if (!container.current) {
+            return () => { }
+        }
+
+        const subscription = subscribe({
+            target: container.current,
+            actions: [
+                {
+                    key: t('json_viewer.copy'),
+                    icon: faCopy,
+                    action: (clickedElement) => {
+                        let parent = clickedElement;
+                        while (parent && parent.tagName !== 'TR') {
+                            parent = parent.parentNode;
                         }
+
+                        const copyValue = parent.dataset.copyValue
+                        toClipboard(copyValue)
                     }
-                ]
-            })
+                }
+            ]
+        })
 
-            return () => {
-                subscription.unsubscribe();
-            }
+        return () => {
+            subscription.unsubscribe();
         }
     }
 
@@ -137,7 +139,7 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy
     }, [searchTerm])
 
     useEffect(() => {
-        registerMenuOptions();
+        const unsubscribe = registerMenuOptions();
 
         if (search.current) {
             search.current.addEventListener('input', handleSearchInput)
@@ -147,6 +149,7 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy
             if (search.current) {
                 search.current.removeEventListener('input', handleSearchInput)
             }
+            unsubscribe()
         }
     }, [])
 
diff --git a/react-ui/src/shared/provider/menu/menu.provider.tsx b/react-ui/src/shared/provider/menu/menu.provider.tsx
index b2525692b..f3af8e5c1 100644
--- a/react-ui/src/shared/provider/menu/menu.provider.tsx
+++ b/react-ui/src/shared/provider/menu/menu.provider.tsx
@@ -19,13 +19,11 @@ type Action = {
 }
 
 interface MenuProviderType {
-    subscribe: (value: SubscriptionValue) => MenuSubscription
+    subscribe: ((value: SubscriptionValue) => MenuSubscription) | null;
 }
 
 const MenuContext = createContext<MenuProviderType>({
-    subscribe: function (): MenuSubscription {
-        throw new Error("Function not implemented.");
-    }
+    subscribe: null
 })
 
 interface SubscriptionValue {
@@ -33,11 +31,16 @@ interface SubscriptionValue {
     actions: Array<Action>
 }
 
+interface SubscriptionMap {
+    [id: string]: SubscriptionValue
+}
+
 
 export const MenuProvider: React.FC<BasicProp> = ({ children }) => {
     const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
     const [showMenu, setShowMenu] = useState(false);
-    const [subscribedTargets, setSubscribedTargets] = useState<Array<SubscriptionValue>>([])
+    const [subscribedTargets, setSubscribedTargets] = useState<SubscriptionMap>({});
+
     const { logout } = useAuth()
 
     const { t } = useTranslation('common')
@@ -61,12 +64,15 @@ export const MenuProvider: React.FC<BasicProp> = ({ children }) => {
 
     const handleContextMenu = (event: React.MouseEvent<HTMLElement>) => {
         event.preventDefault();
-        const targets = subscribedTargets.filter(({ target }) => target.contains(event.target as HTMLElement))
+
+        const targets = Object.values(subscribedTargets).filter(
+            ({ target }) => target.contains(event.target as HTMLElement)
+        );
 
         setMenuPosition({ top: event.pageY, left: event.pageX });
-        setMenuItems(targets)
-        setClickedHtmlElement(event.target as HTMLElement)
-        displayMenu()
+        setMenuItems(targets);
+        setClickedHtmlElement(event.target as HTMLElement);
+        displayMenu();
     };
 
     const displayMenu = () => {
@@ -90,20 +96,27 @@ export const MenuProvider: React.FC<BasicProp> = ({ children }) => {
 
     const value = useMemo<MenuProviderType>(() => {
         return {
-            subscribe(target) {
-                const index = subscribedTargets.length;
+            subscribe(target: SubscriptionValue) {
+                const subscriptionId = crypto.randomUUID(); // Generate unique ID
 
-                setSubscribedTargets([...subscribedTargets, target])
+                setSubscribedTargets(prev => ({
+                    ...prev,
+                    [subscriptionId]: target
+                }));
 
                 const subscription: MenuSubscription = {
                     unsubscribe() {
-                        setSubscribedTargets([...subscribedTargets.splice(index, 1)])
+                        setSubscribedTargets(prev => {
+                            const next = { ...prev };
+                            delete next[subscriptionId];
+                            return next;
+                        });
                     },
                 }
-                return subscription
+                return subscription;
             },
         } as MenuProviderType
-    }, [])
+    }, []);
 
     return (
         <MenuContext.Provider value={value}>
diff --git a/react-ui/yarn.lock b/react-ui/yarn.lock
index 62e8d8fa1..ec57231a8 100755
--- a/react-ui/yarn.lock
+++ b/react-ui/yarn.lock
@@ -4073,6 +4073,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+crypto-js@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
+  integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
+
 crypto-random-string@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
-- 
GitLab


From ea16b7fdaeb102e5aafd3dd19f1c5b5b99d35695 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Thu, 9 Jan 2025 18:42:37 +0100
Subject: [PATCH 28/45] (ui): increase nginx buffer size

---
 react-ui/docker/webserver/nginx.conf | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/react-ui/docker/webserver/nginx.conf b/react-ui/docker/webserver/nginx.conf
index 4ddf7f20d..b9028bec3 100644
--- a/react-ui/docker/webserver/nginx.conf
+++ b/react-ui/docker/webserver/nginx.conf
@@ -25,9 +25,9 @@ http {
 
     # Buffer size settings
     client_body_buffer_size 10K;
-    client_header_buffer_size 1k;
+    client_header_buffer_size 8k;
     client_max_body_size 8m;
-    large_client_header_buffers 2 1k;
+    large_client_header_buffers 4 8k;
 
     # File descriptor cache
     open_file_cache max=2000 inactive=20s;
-- 
GitLab


From e214ef7403ff3a415c9f18a9ede2055296566a9e Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Thu, 9 Jan 2025 18:48:33 +0100
Subject: [PATCH 29/45] (ui): refactor assets

---
 react-ui/assets/logo.svg                      |  17 -----------------
 react-ui/index.html                           |   5 +++--
 react-ui/public/favicon.ico                   | Bin 3870 -> 31533 bytes
 .../protected.layout/protected.layout.tsx     |   2 +-
 react-ui/tsconfig.json                        |   1 -
 react-ui/vite.config.mjs                      |   1 -
 6 files changed, 4 insertions(+), 22 deletions(-)
 delete mode 100755 react-ui/assets/logo.svg
 mode change 100755 => 100644 react-ui/public/favicon.ico

diff --git a/react-ui/assets/logo.svg b/react-ui/assets/logo.svg
deleted file mode 100755
index b7f71bd90..000000000
--- a/react-ui/assets/logo.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Created with Vectornator (http://vectornator.io/) -->
-<svg height="100%" stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="0 0 113.4 212.625" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:vectornator="http://vectornator.io" xmlns:xlink="http://www.w3.org/1999/xlink">
-<defs>
-<radialGradient cx="170.235" cy="146.046" gradientTransform="matrix(1.00001 -8.65109e-05 8.65119e-05 1 -115.465 -116.986)" gradientUnits="userSpaceOnUse" id="RadialGradient" r="217.591">
-<stop offset="0" stop-color="#c456f7"/>
-<stop offset="1" stop-color="#34054a"/>
-</radialGradient>
-<filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="209.692" id="Filter" width="111.957" x="0.722269" y="2.46642">
-<feDropShadow dx="-4.37114e-08" dy="1" flood-color="#050505" flood-opacity="1" in="SourceGraphic" result="Shadow" stdDeviation="1"/>
-</filter>
-</defs>
-<g id="Layer-1" vectornator:layerName="Layer 1">
-<path d="M35.1413 3.47016L35.1416 6.93891L27.1415 6.93961L27.1464 63.3771C21.6096 65.0011 16.4869 67.0445 12.2717 69.6596C-9.35807 83.0789 12.5182 123.232 12.5264 123.222C12.5341 123.213 12.5498 123.2 12.5576 123.191C13.1518 124.609 14.5144 125.761 16.5579 126.534C18.7737 127.372 21.8204 127.855 26.8082 128.408C25.0644 132.318 24.4639 137.732 25.3409 144.846C26.0816 150.854 28.2327 155.309 32.5924 161.533C32.9615 162.06 34.1761 163.796 34.3427 164.033C35.013 164.984 35.5416 165.705 35.9991 166.408C38.1084 169.647 39.1319 172.149 39.3124 174.814C39.3701 175.665 39.3355 180.513 39.251 187.751C39.2362 189.024 39.0999 200.614 39.0959 200.907C37.7122 201.653 36.7523 203.069 36.7525 204.751C36.7527 207.19 38.7205 209.157 41.1591 209.157C43.5978 209.157 45.5653 207.189 45.5651 204.751C45.5649 203.019 44.5477 201.568 43.096 200.844C43.1023 200.341 43.2363 189.045 43.2511 187.782C43.3388 180.268 43.3816 175.583 43.3124 174.563C43.0775 171.096 41.7912 168.012 39.3427 164.251C38.854 163.5 38.3197 162.708 37.6237 161.72C37.4507 161.474 36.2342 159.766 35.8735 159.251C31.8564 153.517 29.9529 149.563 29.3096 144.346C28.2621 135.848 29.4895 130.5 32.027 127.533C32.6413 126.815 33.2789 126.306 33.9019 125.97C34.2334 125.792 34.4442 125.7 34.4956 125.689C35.5735 125.448 36.2365 124.392 35.9954 123.314C35.7543 122.236 34.6982 121.542 33.6202 121.783C33.2264 121.871 32.6655 122.078 31.9953 122.439C31.0669 122.939 30.1635 123.734 29.3079 124.658C23.0392 124.019 20.0922 123.577 17.9951 122.784C16.1482 122.086 15.9773 121.63 16.7137 120.347C25.511 115.923 43.9287 113.157 55.0885 113.156C63.8081 113.155 79.7952 116.873 90.4333 119.622C93.9977 120.542 94.7294 120.755 96.621 121.277C94.2649 123.034 89.4439 124.738 83.3087 125.247C83.0835 125.266 82.9434 125.443 82.7462 125.529C82.6074 125.348 82.5171 125.106 82.3711 124.935C81.4271 123.831 80.3863 122.999 79.3396 122.435C78.6693 122.074 78.1085 121.867 77.7145 121.779C76.6365 121.538 75.5805 122.233 75.3396 123.31C75.0987 124.388 75.7619 125.444 76.8399 125.685C76.8913 125.697 77.1021 125.788 77.4337 125.967C78.0567 126.302 78.6942 126.81 79.3088 127.529C81.8468 130.496 83.0751 135.844 82.029 144.341C81.3867 149.559 79.4839 153.513 75.4678 159.248C75.1071 159.763 73.8909 161.471 73.7179 161.717C73.0221 162.705 72.488 163.498 71.9994 164.248C69.5516 168.009 68.2658 171.094 68.0315 174.561C67.9625 175.581 68.0061 180.266 68.0951 187.78C68.1102 189.055 68.2793 200.673 68.2838 200.967C66.9421 201.725 66.0027 203.098 66.0028 204.749C66.0031 207.187 67.9709 209.155 70.4095 209.155C72.8481 209.154 74.8156 207.187 74.8154 204.748C74.8153 202.975 73.7598 201.482 72.2526 200.779C72.2461 200.277 72.1101 189.009 72.0952 187.748C72.0095 180.511 71.974 175.662 72.0316 174.811C72.2116 172.146 73.2659 169.644 75.3746 166.404C75.8321 165.701 76.3292 164.981 76.9994 164.029C77.1659 163.793 78.3803 162.056 78.7492 161.529C83.1079 155.305 85.2582 150.849 85.9979 144.841C86.8306 138.077 86.2654 132.917 84.7153 129.06C93.073 128.178 99.944 125.471 101.777 121.527C105.258 116.592 120.674 81.4184 100.46 69.1833C96.4145 66.7344 91.6503 64.7196 86.3971 63.1845L86.3922 6.93448L78.3609 6.93517L78.3606 3.46642L35.1413 3.47016ZM31.1419 10.9393L39.1107 10.9386L39.3309 27.9386C39.3406 28.6841 39.9605 29.2606 40.7061 29.2509C41.4516 29.2413 42.0593 28.6214 42.0497 27.8758L41.7983 10.9383L44.517 10.9381L44.7998 27.9381C44.8108 28.6836 45.3981 29.2615 46.1436 29.2505C46.8892 29.2394 47.4983 28.6209 47.4873 27.8753L47.2358 10.9379L49.9546 10.9376L50.2061 27.9376C50.2169 28.6831 50.8357 29.2608 51.5812 29.25C52.3267 29.2392 52.9044 28.6203 52.8936 27.8749L52.6421 10.9374L55.3609 10.9372L55.6124 27.9371C55.623 28.6827 56.242 29.2601 56.9875 29.2495C57.733 29.2389 58.3418 28.6199 58.3312 27.8744L58.0797 10.9369L60.8297 10.9367L61.05 27.9367C61.0546 28.3094 61.2147 28.6381 61.4563 28.8741C61.6979 29.1102 62.0211 29.2537 62.3938 29.2491C63.1394 29.2398 63.7468 28.6194 63.7375 27.8739L63.5173 10.9365L66.2986 10.9362L66.4875 27.905C66.4957 28.6505 67.1171 29.2568 67.8627 29.2486C68.6082 29.2404 69.1834 28.6503 69.1751 27.9047L68.9861 10.936L71.7674 10.9357L71.9564 27.9357C71.9605 28.3085 72.1132 28.6372 72.3627 28.8732C72.6122 29.1091 72.9587 29.2523 73.3315 29.2481C74.077 29.2398 74.6833 28.6498 74.6751 27.9042L74.4862 10.9355L82.3925 10.9348L82.3969 62.2473C78.1953 61.2363 73.8195 60.4151 69.2716 59.936L69.0841 59.936L69.0846 65.9672L62.3352 73.3116L62.211 82.5928L62.212 94.7803L56.5244 94.7808L50.3056 94.7814L50.2733 82.9376L50.1163 73.3126L43.1468 65.8132L43.1463 60.0945L31.1463 62.3455L31.1419 10.9393ZM46.0197 41.188L45.9904 64.3755L52.9599 71.8749L53.1492 91.9374L59.368 91.9368L59.5225 71.8743L66.2407 64.5612L66.2387 41.3425L46.0197 41.188ZM45.5578 121.282C45.0484 121.232 44.5158 121.369 44.0891 121.719C43.2356 122.421 43.1382 123.71 43.8394 124.563C43.8712 124.602 43.9181 124.67 44.0269 124.813C44.2163 125.062 44.4521 125.354 44.6832 125.688C45.3494 126.65 45.998 127.755 46.621 128.938C48.9603 133.379 50.0248 137.836 49.1222 141.875C48.4351 144.95 46.6061 147.711 43.4353 150.126C43.1568 150.338 42.1644 150.942 40.7792 151.751C40.6905 151.803 37.4839 153.662 37.4356 153.689C36.4728 154.23 36.1131 155.445 36.6545 156.408C37.1959 157.37 38.4106 157.699 39.3734 157.157C39.4251 157.128 42.7199 155.272 42.8108 155.22C44.3781 154.304 45.4057 153.669 45.8731 153.313C49.7829 150.336 52.1324 146.76 53.0285 142.75C54.1746 137.622 52.9238 132.266 50.1834 127.063C49.4811 125.729 48.7181 124.526 47.9643 123.438C47.5056 122.775 47.1244 122.271 46.9017 122C46.5511 121.574 46.0673 121.332 45.5578 121.282ZM65.7769 121.28C65.2675 121.33 64.7837 121.572 64.4331 121.999C64.2105 122.27 63.8293 122.774 63.3708 123.436C62.6172 124.525 61.8858 125.728 61.1835 127.062C58.444 132.265 57.163 137.621 58.3099 142.749C59.2067 146.759 61.5569 150.335 65.4671 153.311C65.9346 153.667 66.9935 154.302 68.5611 155.217C68.6519 155.27 71.9158 157.125 71.9675 157.154C72.9304 157.696 74.145 157.367 74.6862 156.404C75.2275 155.441 74.8676 154.227 73.9048 153.686C73.8565 153.658 70.6495 151.8 70.5608 151.748C69.1754 150.939 68.1829 150.336 67.9044 150.124C64.7332 147.71 62.9036 144.948 62.2161 141.874C61.3128 137.835 62.3766 133.378 64.715 128.936C65.3378 127.753 65.9862 126.649 66.6522 125.686C66.8833 125.352 67.1191 125.06 67.3084 124.811C67.4172 124.668 67.4953 124.6 67.5272 124.561C68.2282 123.708 68.0992 122.418 67.2457 121.717C66.8189 121.367 66.2862 121.23 65.7769 121.28ZM28.3735 160.064C27.8641 160.114 27.3492 160.357 26.9985 160.783C24.0604 164.358 22.5881 168.723 22.5621 174.253C22.5614 174.404 22.6311 200.839 22.627 200.877C21.1966 201.608 20.1896 203.036 20.1898 204.753C20.19 207.191 22.1578 209.159 24.5964 209.159C27.0351 209.158 29.0026 207.191 29.0024 204.752C29.0022 203.061 28.0235 201.65 26.627 200.908C26.6562 200.425 26.6538 197.055 26.6259 187.658C26.6229 186.662 26.5615 174.385 26.5622 174.252C26.584 169.618 27.7563 166.157 30.0925 163.314C30.7939 162.461 30.6644 161.203 29.811 160.502C29.3844 160.151 28.8829 160.015 28.3735 160.064ZM82.9679 160.06C82.4585 160.01 81.9571 160.147 81.5304 160.497C80.6772 161.199 80.5479 162.457 81.2494 163.31C83.5862 166.152 84.7591 169.613 84.7817 174.247C84.7823 174.378 84.7241 199.67 84.7527 200.841C83.3011 201.564 82.2529 203.015 82.253 204.747C82.2533 207.186 84.2524 209.153 86.691 209.153C89.1295 209.153 91.0971 207.185 91.0969 204.747C91.0967 203.049 90.1265 201.611 88.7215 200.872C88.7173 200.833 88.7318 194.753 88.7516 187.684C88.7544 186.688 88.7824 174.398 88.7817 174.247C88.7548 168.717 87.2817 164.353 84.343 160.778C83.9922 160.352 83.4774 160.109 82.9679 160.06Z" fill="url(#RadialGradient)" fill-rule="nonzero" filter="url(#Filter)" stroke="none" vectornator:shadowAngle="1.5708" vectornator:shadowColor="#050505" vectornator:shadowOffset="1" vectornator:shadowOpacity="1" vectornator:shadowRadius="2"/>
-</g>
-</svg>
diff --git a/react-ui/index.html b/react-ui/index.html
index 91d50a11c..3dc374a59 100755
--- a/react-ui/index.html
+++ b/react-ui/index.html
@@ -5,13 +5,14 @@
     <link rel="icon" href="favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="theme-color" content="#000000" />
+    <meta name="author" content="Matthias Feyll" />
     <meta
       name="description"
-      content="Web site created using create-react-app"
+      content="goSDN web ui"
     />
     <link rel="apple-touch-icon" href="logo.png" />
     <link rel="manifest" href="manifest.json" />
-    <title>goSDN</title>
+    <title>goSDN - ui</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
diff --git a/react-ui/public/favicon.ico b/react-ui/public/favicon.ico
old mode 100755
new mode 100644
index a11777cc471a4344702741ab1c8a588998b1311a..ae6dcf05241b89f446e88d15b81ffd69202fa83a
GIT binary patch
literal 31533
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
UF6((4fUd^&boFyt=akR{0H5d7UH||9

literal 3870
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
ZtMWJ#1p_C<F-T5Hu2|;y@Dg%;1puB)gNgtE

diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
index b017cf56f..0f0e8161e 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
@@ -1,4 +1,3 @@
-import logo from '@assets/logo.svg';
 import { faCircleUser, faRightFromBracket } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { useAppDispatch, useAppSelector } from '@hooks';
@@ -11,6 +10,7 @@ import { Dropdown } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 import { Link, Outlet, useNavigate } from "react-router-dom";
 import "./protected.layout.scss";
+import logo from '/public/logo.png';
 
 
 export const ProtectedLayout = () => {
diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json
index c679b7100..d40af31cc 100755
--- a/react-ui/tsconfig.json
+++ b/react-ui/tsconfig.json
@@ -22,7 +22,6 @@
 
         "baseUrl": ".",
         "paths": {
-            "@assets/*": ["assets/*"],
             "@api/*": ["src/shared/api/*"],
             "@reducer/*": ["src/stores/reducer/*"],
             "@provider/*": ["src/shared/provider/*"],
diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs
index 83378d6ee..1304138c1 100755
--- a/react-ui/vite.config.mjs
+++ b/react-ui/vite.config.mjs
@@ -58,7 +58,6 @@ export default defineConfig({
     },
     resolve: {
         alias: {
-            '@assets': '/assets',
             '@api': '/src/shared/api',
             '@reducer': '/src/stores/reducer',
             '@provider': '/src/shared/provider',
-- 
GitLab


From d54ff5ba2be4ce59f390a4e0ca1421d658f430b1 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@@stud.h-da.de>
Date: Thu, 9 Jan 2025 19:07:17 +0100
Subject: [PATCH 30/45] (ui): minor css improvements

---
 react-ui/src/components/devices/view/device.scss     |  1 +
 .../components/devices/view/device.view.table.tsx    |  2 --
 .../src/components/devices/view/device.view.tabs.tsx |  2 --
 react-ui/src/components/devices/view/device.view.tsx | 12 ++++++------
 react-ui/vite.config.mjs                             |  1 -
 5 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/react-ui/src/components/devices/view/device.scss b/react-ui/src/components/devices/view/device.scss
index 540cd4d01..866ce81eb 100755
--- a/react-ui/src/components/devices/view/device.scss
+++ b/react-ui/src/components/devices/view/device.scss
@@ -57,5 +57,6 @@
     &.active {
         color: map-get($theme-colors, primary);
         font-weight: 500;
+        text-decoration: underline;
     }
 }
diff --git a/react-ui/src/components/devices/view/device.view.table.tsx b/react-ui/src/components/devices/view/device.view.table.tsx
index 56ad05f3f..8800010e2 100755
--- a/react-ui/src/components/devices/view/device.view.table.tsx
+++ b/react-ui/src/components/devices/view/device.view.table.tsx
@@ -48,7 +48,6 @@ export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) =
                         <td data-copy-value={deviceId} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(cropedId, search) : DOMPurify.sanitize(cropedId) }}></td>
                     </OverlayTrigger>
                     <td data-copy-value={username} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(username, search) : DOMPurify.sanitize(username) }}></td>
-                    <td></td>
                 </tr>
             )
         })
@@ -61,7 +60,6 @@ export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) =
                     <th>{t('device.table.header.name')}</th>
                     <th>{t('device.table.header.uuid')}</th>
                     <th>{t('device.table.header.user')}</th>
-                    <th>{t('device.table.header.last_updated')}</th>
                 </tr>
             </thead>
             <tbody>
diff --git a/react-ui/src/components/devices/view/device.view.tabs.tsx b/react-ui/src/components/devices/view/device.view.tabs.tsx
index a2768a0ea..ef8ba120f 100755
--- a/react-ui/src/components/devices/view/device.view.tabs.tsx
+++ b/react-ui/src/components/devices/view/device.view.tabs.tsx
@@ -24,8 +24,6 @@ export const DeviceViewTabs = (activeTab: DeviceViewTabValues) => {
             <>
                 {jsonYang &&
                     <JsonViewer json={jsonYang} />
-
-                    //<ReactJson src={selectedDevice.json} name={false} collapsed={true} quotesOnKeys={false} />
                 }
             </>
         );
diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx
index a2c8458a7..518c12af4 100755
--- a/react-ui/src/components/devices/view/device.view.tsx
+++ b/react-ui/src/components/devices/view/device.view.tsx
@@ -15,19 +15,19 @@ const DeviceView = () => {
         <div className='m-4 pt-4'>
             <Container fluid>
                 <Row>
-                    <Col sm={5}>
+                    <Col lg={5} sm={12}>
                         <Container className='bg-white rounded c-box'>
                             <Row>
                                 <Col sm={12} className='mt-4'><h3 className='text-black-50'>{t('device.title')}</h3></Col>
                             </Row>
 
                             <Row className='align-items-center'>
-                                <Col sm={6}>
+                                <Col xs={12} sm={6}>
                                     <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'>
                                         <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={searchRef} />
                                     </Form.Group>
                                 </Col>
-                                <Col sm={{ span: 3, offset: 3 }} className='pt-2'>
+                                <Col xs={12} sm={6} className='pt-2'>
                                     <Button variant='primary' className='w-100 my-auto'>{t('device.add_device_button')}</Button>
                                 </Col>
                             </Row>
@@ -39,10 +39,10 @@ const DeviceView = () => {
                             </Row>
                         </Container>
                     </Col>
-                    <Col sm={7}>
+                    <Col xs={12} lg={7} className='mt-5 mt-lg-0'>
                         <Container className='bg-white rounded c-box'>
                             <Row>
-                                <Col sm={12} className='mt-4'>
+                                <Col xs={12} className='mt-4'>
                                     <Nav className='justify-content-around'>
                                         <NavLink className={handleActiveTabLink(DeviceViewTabValues.METADATA) + " tab-links"} onClick={() => setActiveTab(DeviceViewTabValues.METADATA)}>{t('device.tabs.metadata.title')}</NavLink>
                                         <NavLink className={handleActiveTabLink(DeviceViewTabValues.YANGMODEL) + " tab-links"} onClick={() => setActiveTab(DeviceViewTabValues.YANGMODEL)}>{t('device.tabs.yang_model.title')}</NavLink>
@@ -51,7 +51,7 @@ const DeviceView = () => {
                             </Row>
 
                             <Row className='align-items-start'>
-                                <Col sm={12} className='pt-2'>
+                                <Col xs={12}>
                                     {DeviceViewTabs(activeTab)}
                                 </Col>
                             </Row>
diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs
index 1304138c1..33fd69cc7 100755
--- a/react-ui/vite.config.mjs
+++ b/react-ui/vite.config.mjs
@@ -1,7 +1,6 @@
 import react from '@vitejs/plugin-react';
 import { defineConfig } from 'vite';
 
-
 export default defineConfig({
     plugins: [react()],
     build: {
-- 
GitLab


From 4dae015f461a2858fa4e1cfa91926bc7f21b72bb Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@stud.h-da.de>
Date: Thu, 9 Jan 2025 19:10:49 +0100
Subject: [PATCH 31/45] (ui): fix import path

---
 react-ui/src/components/login/view/login.view.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/react-ui/src/components/login/view/login.view.tsx b/react-ui/src/components/login/view/login.view.tsx
index 38afc83a1..62c84eeb8 100755
--- a/react-ui/src/components/login/view/login.view.tsx
+++ b/react-ui/src/components/login/view/login.view.tsx
@@ -1,10 +1,10 @@
-import logo from '@assets/logo.svg'
 import { BasicProp } from '@shared/types/interfaces.type'
 import React, { useRef } from 'react'
 import { Alert, Button, Col, Container, Form, Image, Row, Spinner } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import useLoginViewModel from '../viewmodel/login.viewmodel'
 import './login.scss'
+import logo from '/public/logo.svg'
 
 const LoginView: React.FC<BasicProp> = () => {
     const { t } = useTranslation('common')
-- 
GitLab


From 227b80e7932d21b89ab80164af35a20f9c3721c6 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@stud.h-da.de>
Date: Fri, 10 Jan 2025 01:42:44 +0100
Subject: [PATCH 32/45] (ui): remove sidebar

---
 .../components/devices/view/device.view.tsx   |   4 +-
 react-ui/src/routes.tsx                       |  25 ++--
 .../protected.layout/protected.layout.scss    |  49 ++++++--
 .../protected.layout/protected.layout.tsx     |  20 +--
 react-ui/src/shared/style/box.scss            |  28 +++--
 react-ui/src/shared/style/colors.scss         |  18 +--
 react-ui/src/shared/style/utils.scss          |   4 -
 .../src/shared/utils/loading-fallback.tsx     | 117 ++++++++++++++++++
 8 files changed, 207 insertions(+), 58 deletions(-)
 create mode 100644 react-ui/src/shared/utils/loading-fallback.tsx

diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx
index 518c12af4..4ef007327 100755
--- a/react-ui/src/components/devices/view/device.view.tsx
+++ b/react-ui/src/components/devices/view/device.view.tsx
@@ -16,7 +16,7 @@ const DeviceView = () => {
             <Container fluid>
                 <Row>
                     <Col lg={5} sm={12}>
-                        <Container className='bg-white rounded c-box'>
+                        <Container className='bg-white c-box'>
                             <Row>
                                 <Col sm={12} className='mt-4'><h3 className='text-black-50'>{t('device.title')}</h3></Col>
                             </Row>
@@ -40,7 +40,7 @@ const DeviceView = () => {
                         </Container>
                     </Col>
                     <Col xs={12} lg={7} className='mt-5 mt-lg-0'>
-                        <Container className='bg-white rounded c-box'>
+                        <Container className='bg-white c-box'>
                             <Row>
                                 <Col xs={12} className='mt-4'>
                                     <Nav className='justify-content-around'>
diff --git a/react-ui/src/routes.tsx b/react-ui/src/routes.tsx
index 532d03fbf..a476feaae 100755
--- a/react-ui/src/routes.tsx
+++ b/react-ui/src/routes.tsx
@@ -1,5 +1,6 @@
 import { BasicLayout } from "@layout/basic.layout";
 import { ProtectedLayout } from "@layout/protected.layout/protected.layout";
+import DelayedRender, { SplashScreen } from "@utils/loading-fallback";
 import { lazy, Suspense } from 'react';
 import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom";
 
@@ -10,17 +11,16 @@ export const LOGIN_URL = '/login';
 const DeviceView = lazy(() => import('./components/devices/view/device.view'));
 const LoginLayout = lazy(() => import('./components/login/layouts/login.layout'));
 
-// Loading fallback component
-const LoadingFallback = () => <div>Loading...</div>;
-
 export const router = createBrowserRouter(
     createRoutesFromElements(
         <Route element={<BasicLayout />}>
             <Route
                 path={LOGIN_URL}
                 element={
-                    <Suspense fallback={<LoadingFallback />}>
-                        <LoginLayout />
+                    <Suspense fallback={null}>
+                        <DelayedRender>
+                            <LoginLayout />
+                        </DelayedRender>
                     </Suspense>
                 }
             />
@@ -28,9 +28,16 @@ export const router = createBrowserRouter(
                 <Route
                     path={DEVICE_URL}
                     element={
-                        <Suspense fallback={<LoadingFallback />}>
-                            <DeviceView />
-                        </Suspense>
+                        <DelayedRender
+                            loading={{
+                                minimumLoadingTime: 1000,
+                                component: SplashScreen
+                            }}
+                        >
+                            <Suspense fallback={null}>
+                                <DeviceView />
+                            </Suspense>
+                        </DelayedRender>
                     }
                 />
                 <Route
@@ -38,6 +45,6 @@ export const router = createBrowserRouter(
                     element={<Navigate to={DEVICE_URL} replace={true} />}
                 />
             </Route>
-        </Route>
+        </Route >
     )
 );
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
index 07b38d5a7..713e63481 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
@@ -1,7 +1,5 @@
 @import "/src/shared/style/colors.scss";
 
-$sidebar-width: 4.5em;
-
 .head-links {
     text-decoration: none;
     color: map-get($theme-colors, dark);
@@ -19,11 +17,46 @@ $sidebar-width: 4.5em;
     }
 }
 
-.sidebar {
-    width: $sidebar-width;
-    height: 100vh;
-}
+// Add these styles to your protected.layout.scss
+nav {
+    border-radius: 0 0 $border-radius $border-radius;
+    box-shadow:
+        0px 4px 8px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 35%),
+        0px 2px 4px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 20%);
+
+    .head-links {
+        text-decoration: none;
+        color: map-get($theme-colors, "dark");
+        padding: 8px 16px;
+        margin: 0 4px;
+        border-radius: 12px;
+        transition: all 0.2s ease;
 
-.main-content {
-    margin-left: $sidebar-width;
+        &:hover {
+            background-color: map-get($theme-colors, "bg-primary");
+        }
+
+        &.active {
+            color: map-get($theme-colors, "primary");
+            background-color: map-get($theme-colors, "primary::hover");
+        }
+    }
+
+    .dropdown-menu {
+        border-radius: $border-radius;
+        box-shadow:
+            0px 4px 8px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 35%),
+            0px 2px 4px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 20%);
+        border: none;
+        padding: 8px;
+
+        .dropdown-item {
+            border-radius: 8px;
+            padding: 8px 16px;
+
+            &:hover {
+                background-color: map-get($theme-colors, "bg-primary");
+            }
+        }
+    }
 }
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
index 0f0e8161e..b1d70b823 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
@@ -69,14 +69,6 @@ export const ProtectedLayout = () => {
     }
   );
 
-  const VerticalSidebar = () => {
-    return (
-      <div className="d-flex fixed-top flex-column flex-shrink-0 bg-white sidebar justify-content-end border-end border-dark py-3 z-2">
-        <FontAwesomeIcon className="clickable icon" icon={faRightFromBracket} onClick={logout} size="2x" />
-      </div>
-    )
-  }
-
   const HorizontalNavbar = () => {
     return (
       <nav className="bg-white border-bottom border-dark py-2 d-flex align-items-center z-3 position-relative">
@@ -87,15 +79,18 @@ export const ProtectedLayout = () => {
 
         <Dropdown className="ms-auto px-3">
           <Dropdown.Toggle as={UserIconToggle}>
-            <FontAwesomeIcon icon={faCircleUser} className="icon clickable" />
+            <FontAwesomeIcon icon={faCircleUser} className="clickable" size="2x" />
           </Dropdown.Toggle>
 
           <Dropdown.Menu as={UserIconMenu}>
             <Dropdown.Item eventKey="1">{user?.name}</Dropdown.Item>
             <hr />
-            <Dropdown.Item eventKey="1">
+            <Dropdown.Item eventKey="2">
               <Link className="text-decoration-none text-reset" to="/">{t('protected.link.settings')}</Link>
             </Dropdown.Item>
+            <Dropdown.Item eventKey="3" onClick={logout}>
+              <Link className="text-decoration-none text-reset" to="/"><FontAwesomeIcon className="clickable" icon={faRightFromBracket} />{t('protected.link.settings')}</Link>
+            </Dropdown.Item>
           </Dropdown.Menu>
         </Dropdown>
       </nav>
@@ -106,10 +101,7 @@ export const ProtectedLayout = () => {
     <div>
       <MenuProvider>
         {HorizontalNavbar()}
-        {VerticalSidebar()}
-        <div className='main-content'>
-          <Outlet />
-        </div>
+        <Outlet />
       </MenuProvider>
     </div>
   )
diff --git a/react-ui/src/shared/style/box.scss b/react-ui/src/shared/style/box.scss
index bd75fb00a..934861a2b 100755
--- a/react-ui/src/shared/style/box.scss
+++ b/react-ui/src/shared/style/box.scss
@@ -1,26 +1,30 @@
-@import './colors.scss';
+@import "./colors.scss";
 
 $box-padding: 10px;
 $border-radius: 20px;
- 
+$border-width: 2px;
 
 .c-box {
     padding: $box-padding;
     background-color: white;
-    box-shadow: 0px 4px 4px rgba(0,0,0, .35);
+    position: relative;
     border-radius: $border-radius;
+
+    background:
+        linear-gradient(white, white) padding-box,
+        linear-gradient(
+                180deg,
+                rgba(map-get($theme-colors, "primary"), 0.3) 0%,
+                rgba(map-get($theme-colors, "primary"), 0.1) 100%
+            )
+            border-box;
+    border: $border-width solid transparent;
+
+    box-shadow: 0px 1px 2px rgba(map-get($theme-colors, "dark"), 0.12);
 }
 
 .abstract-box {
     padding: 16px $box-padding;
-    font-size: .90em;
+    font-size: 0.9em;
     border-radius: calc($border-radius / 2);
 }
-
-
-// @each $color, $value in $theme-colors {
-//     .#{$color}-box {
-//         @extend .abstract-box;
-//         background-color: $value !important;
-//     }
-// }
diff --git a/react-ui/src/shared/style/colors.scss b/react-ui/src/shared/style/colors.scss
index 749af9e8e..d91ffb44e 100755
--- a/react-ui/src/shared/style/colors.scss
+++ b/react-ui/src/shared/style/colors.scss
@@ -1,11 +1,11 @@
 $theme-colors: (
-  'primary': #b350e0,
-  'primary::hover': #ddaff3af,
-  'bg-primary': #E1E1E1,
-  'danger': #ffdcdc,
-  'warning': #dbd116,
-  'dark': #595959,
-  'black': #000000,
+  "primary": #b350e0,
+  "primary::hover": #ddaff3af,
+  "bg-primary": #ededed,
+  "danger": #ffdcdc,
+  "warning": #dbd116,
+  "dark": #595959,
+  "black": #000000
 );
-  
-@import '/node_modules/bootstrap/scss/bootstrap';
+
+@import "/node_modules/bootstrap/scss/bootstrap";
diff --git a/react-ui/src/shared/style/utils.scss b/react-ui/src/shared/style/utils.scss
index d8be654f7..d6f34d301 100755
--- a/react-ui/src/shared/style/utils.scss
+++ b/react-ui/src/shared/style/utils.scss
@@ -7,7 +7,3 @@
         cursor: pointer;
     }
 }
-
-.icon {
-    font-size: 1.75em;
-}
diff --git a/react-ui/src/shared/utils/loading-fallback.tsx b/react-ui/src/shared/utils/loading-fallback.tsx
new file mode 100644
index 000000000..d1c6daa4a
--- /dev/null
+++ b/react-ui/src/shared/utils/loading-fallback.tsx
@@ -0,0 +1,117 @@
+import React, { useEffect, useState } from 'react';
+import { Col, Container, Row } from 'react-bootstrap';
+import logo from '/public/logo.png';
+
+interface DelayedRenderProps {
+    children: React.ReactNode;
+    loading?: {
+        minimumLoadingTime: number;
+        component: () => JSX.Element
+    }
+}
+
+export const SplashScreen = () => {
+    const [dots, setDots] = useState('');
+
+    useEffect(() => {
+        const dotsInterval = setInterval(() => {
+            setDots(prev => prev.length >= 3 ? '' : prev + '.');
+        }, 500);
+
+        return () => clearInterval(dotsInterval);
+    }, []);
+
+    return (
+        <div className="splash-screen-overlay">
+            <Container fluid className="h-100 d-flex align-items-center justify-content-center bg-bg-primary">
+                <Row>
+                    <Col className="text-center">
+                        <div className="loading-bounce mb-4">
+                            <img
+                                src={logo}
+                                alt="Logo"
+                                className="img-fluid"
+                                style={{ width: '120px', height: '120px', objectFit: 'contain' }}
+                            />
+                        </div>
+                        <div className="loading-text">
+                            <span className="h4 text-secondary">Loading</span>
+                            <span className="h4 text-secondary dots-width">{dots}</span>
+                        </div>
+                    </Col>
+                </Row>
+            </Container>
+
+            <style>
+                {`
+                    .splash-screen-overlay {
+                        position: fixed;
+                        top: 0;
+                        left: 0;
+                        width: 100%;
+                        height: 100%;
+                        background-color: #f8f9fa;
+                        z-index: 0;
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                    }
+
+                    .loading-bounce {
+                        animation: bounce 1s infinite;
+                    }
+
+                    @keyframes bounce {
+                        0%, 100% {
+                            transform: translateY(0);
+                        }
+                        50% {
+                            transform: translateY(-20px);
+                        }
+                    }
+
+                    .loading-text {
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                    }
+
+                    .dots-width {
+                        min-width: 24px;
+                        text-align: left;
+                        margin-left: 2px;
+                    }
+                `}
+            </style>
+        </div>
+    );
+};
+
+export const DelayedRender: React.FC<DelayedRenderProps> = ({
+    children,
+    loading
+}) => {
+    const [shouldRender, setShouldRender] = useState(false);
+
+    useEffect(() => {
+        if (!loading) {
+            setShouldRender(true);
+            return;
+        }
+
+        const timer = setTimeout(() => {
+            setShouldRender(true);
+        }, loading.minimumLoadingTime);
+
+        return () => clearTimeout(timer);
+    }, [loading]);
+
+    if (!shouldRender && loading) {
+        const LoadingComponent = loading.component;
+        return <LoadingComponent />;
+    }
+
+    return <>{children}</>;
+};
+
+export default DelayedRender;
\ No newline at end of file
-- 
GitLab


From 6d1785b9dec02bbfa58c65280aa4118527585b22 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Fri, 10 Jan 2025 08:29:27 +0000
Subject: [PATCH 33/45] [renovate] Update github.com/aristanetworks/goarista
 digest to 1f88a86

See merge request danet/gosdn!1151

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 10 +++++-----
 go.sum | 10 ++++++++++
 2 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/go.mod b/go.mod
index 7c8225cdc..16daa3076 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module code.fbi.h-da.de/danet/gosdn
 go 1.23
 
 require (
-	github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029
+	github.com/aristanetworks/goarista v0.0.0-20250108234106-1f88a86e2265
 	github.com/c-bata/go-prompt v0.2.6
 	github.com/docker/docker v24.0.9+incompatible
 	github.com/google/go-cmp v0.6.0
@@ -47,7 +47,7 @@ require (
 	github.com/gookit/color v1.5.4 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/klauspost/compress v1.17.9 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
@@ -64,7 +64,7 @@ require (
 	github.com/pkg/term v1.2.0-beta.2 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.57.0 // indirect
+	github.com/prometheus/common v0.61.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rabbitmq/amqp091-go v1.10.0
 	github.com/rivo/uniseg v0.4.4 // indirect
@@ -78,7 +78,7 @@ require (
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
 	golang.org/x/crypto v0.32.0
-	golang.org/x/net v0.33.0
+	golang.org/x/net v0.34.0
 	golang.org/x/sys v0.29.0 // indirect
 	golang.org/x/term v0.28.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
@@ -123,6 +123,6 @@ require (
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
 	gotest.tools/v3 v3.5.1 // indirect
 )
diff --git a/go.sum b/go.sum
index eb454ef9b..d1247f175 100644
--- a/go.sum
+++ b/go.sum
@@ -62,6 +62,8 @@ github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44 h1:vb3HPPa
 github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
 github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029 h1:bvw2TILeXtuYfZ9rip/4DY933UuIvCwtvJmwvz978ac=
 github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
+github.com/aristanetworks/goarista v0.0.0-20250108234106-1f88a86e2265 h1:NPQhasGGtAIxtDG4KQTcQviV9T6a98kbKSO0VKFRS+E=
+github.com/aristanetworks/goarista v0.0.0-20250108234106-1f88a86e2265/go.mod h1:1xldiSdHhqa1XIr6EPNnSBfwZEAMZwwJIiEtMS8yzkU=
 github.com/aristanetworks/gomap v0.0.0-20240724180630-b4cffb90720f h1:3GwV1IeLp0PwWcnbc9ZihE3osvexJf3PMjWSCGjtIqc=
 github.com/aristanetworks/gomap v0.0.0-20240724180630-b4cffb90720f/go.mod h1:bNzH6HFWav8D/ws3QlkjLpf9ZOdsUTDx+qJikWCcGRc=
 github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
@@ -198,6 +200,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -306,6 +310,8 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY=
 github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI=
+github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
+github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/protocolbuffers/txtpbfmt v0.0.0-20220608084003-fc78c767cd6a/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78=
@@ -464,6 +470,8 @@ golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
 golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
 golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -625,6 +633,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-- 
GitLab


From 3182bc05a4131fe9248a6074517159caa9c3a2af Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@stud.h-da.de>
Date: Fri, 10 Jan 2025 10:38:32 +0100
Subject: [PATCH 34/45] (ui): refactor navbar styles

---
 .../login/viewmodel/login.viewmodel.ts        |  6 +--
 .../protected.layout/protected.layout.scss    | 15 +++---
 .../protected.layout/protected.layout.tsx     | 50 +++++++++++--------
 react-ui/src/shared/style/box.scss            |  4 +-
 react-ui/src/shared/style/colors.scss         |  2 +
 5 files changed, 42 insertions(+), 35 deletions(-)

diff --git a/react-ui/src/components/login/viewmodel/login.viewmodel.ts b/react-ui/src/components/login/viewmodel/login.viewmodel.ts
index 60c39b55a..fabb0a861 100755
--- a/react-ui/src/components/login/viewmodel/login.viewmodel.ts
+++ b/react-ui/src/components/login/viewmodel/login.viewmodel.ts
@@ -7,9 +7,9 @@ export interface PageLoginState {
 }
 
 export default function useLoginViewModel() {
-    const {login, loginProperties} = useAuth();
-    const {isLoading: loginLoading, error: loginError, reset: resetLogin} = loginProperties!;
-    
+    const { login, loginProperties } = useAuth();
+    const { isLoading: loginLoading, error: loginError, reset: resetLogin } = loginProperties;
+
     const [localFormState, updateLocalFormState] = useState({
         submitted: false,
         valid: false,
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
index 713e63481..52429031b 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
@@ -8,7 +8,6 @@
 
     &:hover {
         color: map-get($theme-colors, primary);
-        font-weight: 600;
     }
 
     &.active {
@@ -17,12 +16,14 @@
     }
 }
 
+#navbar {
+    padding: 1em !important;
+}
+
 // Add these styles to your protected.layout.scss
 nav {
-    border-radius: 0 0 $border-radius $border-radius;
-    box-shadow:
-        0px 4px 8px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 35%),
-        0px 2px 4px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 20%);
+    border-radius: $border-radius $border-radius;
+    padding: 0 !important;
 
     .head-links {
         text-decoration: none;
@@ -44,9 +45,7 @@ nav {
 
     .dropdown-menu {
         border-radius: $border-radius;
-        box-shadow:
-            0px 4px 8px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 35%),
-            0px 2px 4px mix(map-get($theme-colors, "primary"), map-get($theme-colors, "dark"), 20%);
+        box-shadow: $box-shadow;
         border: none;
         padding: 8px;
 
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
index b1d70b823..5dc565455 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
@@ -6,7 +6,7 @@ import { MenuProvider } from '@provider/menu/menu.provider';
 import { DEVICE_URL, LOGIN_URL } from '@routes';
 import { fetchPnds, fetchUser } from '@shared/routine/user.routine';
 import React, { useEffect } from "react";
-import { Dropdown } from "react-bootstrap";
+import { Col, Container, Dropdown, Row } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 import { Link, Outlet, useNavigate } from "react-router-dom";
 import "./protected.layout.scss";
@@ -71,29 +71,35 @@ export const ProtectedLayout = () => {
 
   const HorizontalNavbar = () => {
     return (
-      <nav className="bg-white border-bottom border-dark py-2 d-flex align-items-center z-3 position-relative">
-        <Link to="/"><img src={logo} className="mx-4 me-5" width={25} alt="logo" /></Link>
-        <Link className={"head-links" + handleActiveLink(DEVICE_URL)} to="/">{t('protected.link.device_list')}</Link>
-        <Link className={"head-links" + handleActiveLink('/map')} to="/">{t('protected.link.map')}</Link>
-        <Link className={"head-links" + handleActiveLink('/configuration_management')} to="/">{t('protected.link.configuration_mgmt')}</Link>
+      <Container fluid>
+        <Row>
+          <Col>
+            <nav id="navbar" className="bg-white mx-4 mt-4 d-flex align-items-center c-box">
+              <Link to="/"><img src={logo} className="mx-4" width={45} alt="logo" /></Link>
+              <Link className={"head-links" + handleActiveLink(DEVICE_URL)} to="/">{t('protected.link.device_list')}</Link>
+              <Link className={"head-links" + handleActiveLink('/map')} to="/">{t('protected.link.map')}</Link>
+              <Link className={"head-links" + handleActiveLink('/configuration_management')} to="/">{t('protected.link.configuration_mgmt')}</Link>
 
-        <Dropdown className="ms-auto px-3">
-          <Dropdown.Toggle as={UserIconToggle}>
-            <FontAwesomeIcon icon={faCircleUser} className="clickable" size="2x" />
-          </Dropdown.Toggle>
+              <Dropdown className="ms-auto px-3">
+                <Dropdown.Toggle as={UserIconToggle}>
+                  <FontAwesomeIcon icon={faCircleUser} className="clickable" size="2x" />
+                </Dropdown.Toggle>
 
-          <Dropdown.Menu as={UserIconMenu}>
-            <Dropdown.Item eventKey="1">{user?.name}</Dropdown.Item>
-            <hr />
-            <Dropdown.Item eventKey="2">
-              <Link className="text-decoration-none text-reset" to="/">{t('protected.link.settings')}</Link>
-            </Dropdown.Item>
-            <Dropdown.Item eventKey="3" onClick={logout}>
-              <Link className="text-decoration-none text-reset" to="/"><FontAwesomeIcon className="clickable" icon={faRightFromBracket} />{t('protected.link.settings')}</Link>
-            </Dropdown.Item>
-          </Dropdown.Menu>
-        </Dropdown>
-      </nav>
+                <Dropdown.Menu as={UserIconMenu}>
+                  <Dropdown.Item eventKey="1">{user?.name}</Dropdown.Item>
+                  <hr />
+                  <Dropdown.Item eventKey="2">
+                    <Link className="text-decoration-none text-reset" to="/">{t('protected.link.settings')}</Link>
+                  </Dropdown.Item>
+                  <Dropdown.Item eventKey="3" onClick={logout}>
+                    <Link className="text-decoration-none text-reset" to="/"><FontAwesomeIcon className="clickable" icon={faRightFromBracket} /><span className="ms-1">{t('global.menu_item.logout')}</span></Link>
+                  </Dropdown.Item>
+                </Dropdown.Menu>
+              </Dropdown>
+            </nav>
+          </Col>
+        </Row>
+      </Container>
     )
   }
 
diff --git a/react-ui/src/shared/style/box.scss b/react-ui/src/shared/style/box.scss
index 934861a2b..418fe4af0 100755
--- a/react-ui/src/shared/style/box.scss
+++ b/react-ui/src/shared/style/box.scss
@@ -20,11 +20,11 @@ $border-width: 2px;
             border-box;
     border: $border-width solid transparent;
 
-    box-shadow: 0px 1px 2px rgba(map-get($theme-colors, "dark"), 0.12);
+    box-shadow: $box-shadow;
 }
 
 .abstract-box {
-    padding: 16px $box-padding;
+    padding: $box-padding;
     font-size: 0.9em;
     border-radius: calc($border-radius / 2);
 }
diff --git a/react-ui/src/shared/style/colors.scss b/react-ui/src/shared/style/colors.scss
index d91ffb44e..29c971f86 100755
--- a/react-ui/src/shared/style/colors.scss
+++ b/react-ui/src/shared/style/colors.scss
@@ -8,4 +8,6 @@ $theme-colors: (
   "black": #000000
 );
 
+$box-shadow: 0px 4px 8px rgba(map-get($theme-colors, "primary"), 0.2);
+
 @import "/node_modules/bootstrap/scss/bootstrap";
-- 
GitLab


From 7725832e23dd62eb7e2f42892aa179cc309219db Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@stud.h-da.de>
Date: Fri, 10 Jan 2025 19:45:07 +0100
Subject: [PATCH 35/45] (ui): implement movable box containers

---
 react-ui/package.json                         |  2 +
 .../src/components/devices/view/device.scss   |  5 --
 .../devices/view/device.view.table.tsx        |  1 +
 .../components/devices/view/device.view.tsx   | 50 ++++++++----
 react-ui/src/index.scss                       |  2 +-
 .../layouts/grid.layout/grid.layout.scss      | 81 +++++++++++++++++++
 .../layouts/grid.layout/grid.layout.tsx       | 61 ++++++++++++++
 .../protected.layout/protected.layout.tsx     | 10 +--
 react-ui/src/shared/style/box.scss            | 35 +++++++-
 react-ui/yarn.lock                            | 54 ++++++++++++-
 10 files changed, 268 insertions(+), 33 deletions(-)
 create mode 100644 react-ui/src/shared/layouts/grid.layout/grid.layout.scss
 create mode 100644 react-ui/src/shared/layouts/grid.layout/grid.layout.tsx

diff --git a/react-ui/package.json b/react-ui/package.json
index 30db7c97c..1f742d78d 100755
--- a/react-ui/package.json
+++ b/react-ui/package.json
@@ -14,6 +14,7 @@
         "@fortawesome/react-fontawesome": "^0.2.2",
         "@fullhuman/postcss-purgecss": "^7.0.2",
         "@reduxjs/toolkit": "^2.2.4",
+        "@types/react-grid-layout": "^1.3.5",
         "@vitejs/plugin-react": "^4.2.1",
         "bootstrap": "^5.3.3",
         "crypto-js": "^4.2.0",
@@ -24,6 +25,7 @@
         "react-bootstrap": "^2.10.2",
         "react-dom": "^18.3.1",
         "react-error-boundary": "^4.1.2",
+        "react-grid-layout": "^1.5.0",
         "react-i18next": "^15.0.0",
         "react-redux": "^9.1.2",
         "react-router-dom": "^6.23.1",
diff --git a/react-ui/src/components/devices/view/device.scss b/react-ui/src/components/devices/view/device.scss
index 866ce81eb..8d4099fc8 100755
--- a/react-ui/src/components/devices/view/device.scss
+++ b/react-ui/src/components/devices/view/device.scss
@@ -24,11 +24,6 @@
     }
 }
 
-.c-box {
-    padding: 2em !important;
-    padding-top: 1em !important;
-}
-
 .border-right {
     $border-padding: 2em;
 
diff --git a/react-ui/src/components/devices/view/device.view.table.tsx b/react-ui/src/components/devices/view/device.view.table.tsx
index 8800010e2..1f7221ead 100755
--- a/react-ui/src/components/devices/view/device.view.table.tsx
+++ b/react-ui/src/components/devices/view/device.view.table.tsx
@@ -49,6 +49,7 @@ export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) =
                     </OverlayTrigger>
                     <td data-copy-value={username} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(username, search) : DOMPurify.sanitize(username) }}></td>
                 </tr>
+
             )
         })
     }, [devices, searchRef, pnds, selectedDevice, trClickHandler]);
diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx
index 4ef007327..f705aa4eb 100755
--- a/react-ui/src/components/devices/view/device.view.tsx
+++ b/react-ui/src/components/devices/view/device.view.tsx
@@ -1,3 +1,6 @@
+import { faGripVertical } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { GridLayout } from '@layout/grid.layout/grid.layout';
 import { useRef } from 'react';
 import { Button, Col, Container, Form, Nav, NavLink, Row } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
@@ -13,12 +16,15 @@ const DeviceView = () => {
 
     return (
         <div className='m-4 pt-4'>
-            <Container fluid>
-                <Row>
-                    <Col lg={5} sm={12}>
-                        <Container className='bg-white c-box'>
+            <GridLayout>
+                <>
+                    <div key="device-list">
+                        <Container className='c-box hoverable h-100'>
+                            <FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
                             <Row>
-                                <Col sm={12} className='mt-4'><h3 className='text-black-50'>{t('device.title')}</h3></Col>
+                                <Col sm={12} className='mt-4'>
+                                    <h3 className='text-black-50'>{t('device.title')}</h3>
+                                </Col>
                             </Row>
 
                             <Row className='align-items-center'>
@@ -38,14 +44,26 @@ const DeviceView = () => {
                                 </Col>
                             </Row>
                         </Container>
-                    </Col>
-                    <Col xs={12} lg={7} className='mt-5 mt-lg-0'>
-                        <Container className='bg-white c-box'>
+                    </div>
+
+                    <div key="device-details">
+                        <Container className='c-box hoverable h-100'>
+                            <FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
                             <Row>
                                 <Col xs={12} className='mt-4'>
                                     <Nav className='justify-content-around'>
-                                        <NavLink className={handleActiveTabLink(DeviceViewTabValues.METADATA) + " tab-links"} onClick={() => setActiveTab(DeviceViewTabValues.METADATA)}>{t('device.tabs.metadata.title')}</NavLink>
-                                        <NavLink className={handleActiveTabLink(DeviceViewTabValues.YANGMODEL) + " tab-links"} onClick={() => setActiveTab(DeviceViewTabValues.YANGMODEL)}>{t('device.tabs.yang_model.title')}</NavLink>
+                                        <NavLink
+                                            className={handleActiveTabLink(DeviceViewTabValues.METADATA) + " tab-links"}
+                                            onClick={() => setActiveTab(DeviceViewTabValues.METADATA)}
+                                        >
+                                            {t('device.tabs.metadata.title')}
+                                        </NavLink>
+                                        <NavLink
+                                            className={handleActiveTabLink(DeviceViewTabValues.YANGMODEL) + " tab-links"}
+                                            onClick={() => setActiveTab(DeviceViewTabValues.YANGMODEL)}
+                                        >
+                                            {t('device.tabs.yang_model.title')}
+                                        </NavLink>
                                     </Nav>
                                 </Col>
                             </Row>
@@ -56,11 +74,11 @@ const DeviceView = () => {
                                 </Col>
                             </Row>
                         </Container>
-                    </Col>
-                </Row>
-            </Container>
+                    </div>
+                </>
+            </GridLayout>
         </div>
-    )
-}
+    );
+};
 
-export default DeviceView
+export default DeviceView;
\ No newline at end of file
diff --git a/react-ui/src/index.scss b/react-ui/src/index.scss
index 8dd280e64..5c9f184b7 100755
--- a/react-ui/src/index.scss
+++ b/react-ui/src/index.scss
@@ -1,4 +1,4 @@
-@import './shared/style/index.scss';
+@import "./shared/style/index.scss";
 
 body {
     margin: 0;
diff --git a/react-ui/src/shared/layouts/grid.layout/grid.layout.scss b/react-ui/src/shared/layouts/grid.layout/grid.layout.scss
new file mode 100644
index 000000000..c90375d37
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/grid.layout.scss
@@ -0,0 +1,81 @@
+@import "/src/shared/style/colors.scss";
+
+.drag-handle {
+    position: absolute;
+    top: 0;
+    right: 0;
+    padding: 10px;
+    cursor: grab;
+    color: map-get($theme-colors, "dark");
+    background-color: lighten(map-get($theme-colors, primary), 38%);
+    border-radius: 0 0.25rem 0 0.25rem;
+    border-left: 1px solid lighten(map-get($theme-colors, dark), 35%);
+    border-bottom: 1px solid lighten(map-get($theme-colors, dark), 35%);
+    z-index: 10;
+
+    &:hover {
+        color: map-get($theme-colors, primary);
+        background-color: lighten(map-get($theme-colors, primary), 35%);
+    }
+
+    &:active {
+        cursor: grabbing;
+    }
+}
+
+.react-grid-item {
+    min-height: 600px !important;
+
+    &.react-draggable-dragging {
+        z-index: 100;
+
+        .drag-handle {
+            cursor: grabbing;
+        }
+    }
+}
+
+.react-grid-layout {
+    width: 100% !important;
+}
+
+.react-grid-item.react-grid-placeholder {
+    background: lighten(map-get($theme-colors, primary), 30%) !important;
+    opacity: 0.2;
+    transition-duration: 100ms;
+    z-index: 2;
+    border-radius: 4px;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    -o-user-select: none;
+    user-select: none;
+}
+
+.react-grid-item {
+    /* Hide resize handle by default */
+    .react-resizable-handle-se {
+        opacity: 0;
+        transition: opacity 0.2s ease-in-out;
+    }
+
+    /* Show resize handle on container hover */
+    &:hover .react-resizable-handle-se {
+        opacity: 1;
+    }
+}
+
+/* Style the resize handle */
+.react-resizable-handle-se {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 20px;
+    height: 20px;
+    background-image: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiPjxwYXRoIGQ9Ik02IDZIMHYtNmg2djZ6TTUgMUgxdjRoNFYxeiIgZmlsbD0iIzk5OTk5OSIvPjwvc3ZnPg==");
+    background-position: bottom right;
+    background-repeat: no-repeat;
+    background-size: 10px 10px;
+    cursor: se-resize;
+    z-index: 10;
+}
diff --git a/react-ui/src/shared/layouts/grid.layout/grid.layout.tsx b/react-ui/src/shared/layouts/grid.layout/grid.layout.tsx
new file mode 100644
index 000000000..c184e655b
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/grid.layout.tsx
@@ -0,0 +1,61 @@
+import React, { ReactElement, useEffect, useState } from 'react';
+import { Responsive, WidthProvider } from 'react-grid-layout';
+import 'react-grid-layout/css/styles.css';
+import 'react-resizable/css/styles.css';
+import './grid.layout.scss';
+
+const ResponsiveGridLayout = WidthProvider(Responsive);
+
+interface GridLayoutProps {
+    children: ReactElement;
+}
+
+export const GridLayout: React.FC<GridLayoutProps> = ({ children }) => {
+    const rowHeight = 50;
+    const [mounted, setMounted] = useState(false);
+    const layouts = {
+        lg: [
+            { i: 'device-list', x: 0, y: 0, w: 1, h: 1, minW: 1, minH: 1 },
+            { i: 'device-details', x: 2, y: 0, w: 2, h: 1, minW: 2, minH: 1 }
+        ]
+    };
+
+    useEffect(() => {
+        setMounted(true);
+        // Force layout recalculation after mount
+        window.dispatchEvent(new Event('resize'));
+    }, []);
+
+    const gridItems = React.Children.map(children.props.children, (child, index) => {
+        if (!React.isValidElement(child)) return null;
+
+        return React.cloneElement(child, {
+            key: index === 0 ? 'device-list' : 'device-details',
+            'data-grid': layouts.lg[index]
+        });
+    });
+
+    return (
+        <div style={{ display: mounted ? 'block' : 'none' }}>
+            <ResponsiveGridLayout
+                className="layout"
+                layouts={layouts}
+                breakpoints={{ lg: 996, sm: 480 }}
+                cols={{ lg: 4, sm: 3 }}
+                rowHeight={rowHeight}
+                margin={[20, 20]}
+                draggableHandle=".drag-handle"
+                isDraggable={true}
+                isResizable={true}
+                preventCollision={true}
+                compactType={null}
+                useCSSTransforms={mounted}
+                resizeHandles={['se']} // Only show resize handle in bottom right corner
+            >
+                {gridItems}
+            </ResponsiveGridLayout>
+        </div>
+    );
+};
+
+export default GridLayout;
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
index 5dc565455..dbcb49192 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
@@ -104,11 +104,9 @@ export const ProtectedLayout = () => {
   }
 
   return (
-    <div>
-      <MenuProvider>
-        {HorizontalNavbar()}
-        <Outlet />
-      </MenuProvider>
-    </div>
+    <MenuProvider>
+      {HorizontalNavbar()}
+      <Outlet />
+    </MenuProvider>
   )
 };
\ No newline at end of file
diff --git a/react-ui/src/shared/style/box.scss b/react-ui/src/shared/style/box.scss
index 418fe4af0..53158f515 100755
--- a/react-ui/src/shared/style/box.scss
+++ b/react-ui/src/shared/style/box.scss
@@ -1,26 +1,55 @@
 @import "./colors.scss";
 
 $box-padding: 10px;
-$border-radius: 20px;
+$border-radius: 0.25em;
 $border-width: 2px;
+$transition-duration: 0.3s;
 
 .c-box {
     padding: $box-padding;
     background-color: white;
     position: relative;
     border-radius: $border-radius;
+    transition: box-shadow $transition-duration ease-in-out;
 
     background:
         linear-gradient(white, white) padding-box,
         linear-gradient(
                 180deg,
-                rgba(map-get($theme-colors, "primary"), 0.3) 0%,
+                rgba(map-get($theme-colors, "primary"), 0.4) 0%,
+                rgba(map-get($theme-colors, "primary"), 0.2) 40%,
                 rgba(map-get($theme-colors, "primary"), 0.1) 100%
             )
             border-box;
     border: $border-width solid transparent;
-
     box-shadow: $box-shadow;
+
+    &::before {
+        content: "";
+        position: absolute;
+        top: -$border-width;
+        left: -$border-width;
+        right: -$border-width;
+        bottom: -$border-width;
+        background: linear-gradient(
+            180deg,
+            rgba(map-get($theme-colors, "primary"), 0.4) 0%,
+            rgba(map-get($theme-colors, "primary"), 0.2) 60%,
+            rgba(map-get($theme-colors, "primary"), 0.1) 100%
+        );
+        border-radius: inherit;
+        z-index: -1;
+        opacity: 0;
+        transition: opacity $transition-duration ease-in-out;
+    }
+
+    &:hover {
+        box-shadow: 0 0.5rem 1rem rgba(map-get($theme-colors, "primary"), 0.2);
+
+        &::before {
+            opacity: 1;
+        }
+    }
 }
 
 .abstract-box {
diff --git a/react-ui/yarn.lock b/react-ui/yarn.lock
index ec57231a8..db8fb7d3c 100755
--- a/react-ui/yarn.lock
+++ b/react-ui/yarn.lock
@@ -2593,6 +2593,13 @@
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.3.tgz#3654138d0da1b0c7916f6ed0dc1cc2b576d47650"
   integrity sha512-uTYkxTLkYp41nq/ULXyXMtkNT1vu5fXJoqad6uTNCOGat5t9cLgF4vMNLBXsTOXpdOI44XzKPY1M5RRm0bQHuw==
 
+"@types/react-grid-layout@^1.3.5":
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz#f4b52bf27775290ee0523214be0987be14e66823"
+  integrity sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-transition-group@^4.4.6":
   version "4.4.11"
   resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5"
@@ -3843,7 +3850,12 @@ cliui@^8.0.1:
     strip-ansi "^6.0.1"
     wrap-ansi "^7.0.0"
 
-clsx@^2.1.0:
+clsx@^1.1.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+  integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
+clsx@^2.0.0, clsx@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
   integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -5351,6 +5363,11 @@ fast-diff@^1.1.2:
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
   integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
 
+fast-equals@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7"
+  integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==
+
 fast-glob@^3.2.9, fast-glob@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -8680,7 +8697,7 @@ prop-types-extra@^1.1.0:
     react-is "^16.3.2"
     warning "^4.0.0"
 
-prop-types@^15.6.2, prop-types@^15.8.1:
+prop-types@15.x, prop-types@^15.6.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -8838,6 +8855,14 @@ react-dom@^18.3.1:
     loose-envify "^1.1.0"
     scheduler "^0.23.2"
 
+react-draggable@^4.0.3, react-draggable@^4.4.5:
+  version "4.4.6"
+  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e"
+  integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
+  dependencies:
+    clsx "^1.1.1"
+    prop-types "^15.8.1"
+
 react-error-boundary@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289"
@@ -8850,6 +8875,18 @@ react-error-overlay@^6.0.11:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
   integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
 
+react-grid-layout@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.5.0.tgz#b6cc9412b58cf8226aebc0df7673d6fa782bdee2"
+  integrity sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==
+  dependencies:
+    clsx "^2.0.0"
+    fast-equals "^4.0.3"
+    prop-types "^15.8.1"
+    react-draggable "^4.4.5"
+    react-resizable "^3.0.5"
+    resize-observer-polyfill "^1.5.1"
+
 react-i18next@^15.0.0:
   version "15.1.4"
   resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.4.tgz#65c03c31a5e42202000652e163f22f23a9306a60"
@@ -8896,6 +8933,14 @@ react-refresh@^0.14.2:
   resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
   integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
 
+react-resizable@^3.0.5:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1"
+  integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==
+  dependencies:
+    prop-types "15.x"
+    react-draggable "^4.0.3"
+
 react-router-dom@^6.23.1:
   version "6.28.0"
   resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.28.0.tgz#f73ebb3490e59ac9f299377062ad1d10a9f579e6"
@@ -9189,6 +9234,11 @@ reselect@^5.1.0:
   resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
   integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-cwd@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
-- 
GitLab


From e59acfcd4d8a41a4508b2963e649c845a5374d29 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Mon, 13 Jan 2025 07:53:54 +0000
Subject: [PATCH 36/45] [renovate] Update module go.mongodb.org/mongo-driver to
 v1.17.2

See merge request danet/gosdn!1147

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 16daa3076..ef618f305 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/spf13/viper v1.19.0
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/stretchr/testify v1.10.0
-	go.mongodb.org/mongo-driver v1.17.1
+	go.mongodb.org/mongo-driver v1.17.2
 	golang.org/x/sync v0.10.0
 	google.golang.org/grpc v1.69.2
 	google.golang.org/protobuf v1.36.2
diff --git a/go.sum b/go.sum
index d1247f175..fc4bb17f3 100644
--- a/go.sum
+++ b/go.sum
@@ -406,6 +406,8 @@ go.mongodb.org/mongo-driver v1.17.0 h1:Hp4q2MCjvY19ViwimTs00wHi7G4yzxh4/2+nTx8r4
 go.mongodb.org/mongo-driver v1.17.0/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
 go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
 go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
+go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
+go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-- 
GitLab


From 91b6db7bbc1fe5c5bc4b21d1fdf39bec3cdce699 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Mon, 13 Jan 2025 08:04:48 +0000
Subject: [PATCH 37/45] [renovate] Update renovate/renovate Docker tag to
 v39.106.0

See merge request danet/gosdn!1153

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.renovate.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.renovate.yml b/.gitlab/ci/.renovate.yml
index 03f8fddfa..4d7442638 100644
--- a/.gitlab/ci/.renovate.yml
+++ b/.gitlab/ci/.renovate.yml
@@ -1,7 +1,7 @@
 renovate:
     stage: tools
 
-    image: renovate/renovate:39.91.3
+    image: renovate/renovate:39.106.0
 
     variables:
         LOG_LEVEL: debug
-- 
GitLab


From 671ed78c14074082c33ea57b412e89e457178d6e Mon Sep 17 00:00:00 2001
From: Fabian Seidl <fabian.seidl@h-da.de>
Date: Mon, 13 Jan 2025 09:24:45 +0000
Subject: [PATCH 38/45] Bump golangci-lint in Makefile to version 1.63.4 and
 fix deprecated output option

See merge request danet/gosdn!1140
---
 .golangci.yml | 2 +-
 Makefile      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.golangci.yml b/.golangci.yml
index cdd404e55..42bb6ac6b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -14,13 +14,13 @@ output:
       - format: colored-line-number
     print-issued-lines: true
     print-linter-name: true
-    uniq-by-line: true
     path-prefix: ""
 
 issues:
     exclude-use-default: false
     max-issues-per-linter: 0
     max-same-issues: 0
+    uniq-by-line: true
     exclude-files:
         - http.go
     # directories to be ignored by linters
diff --git a/Makefile b/Makefile
index 888644abe..0cd0ed66d 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ PLUGIN_NAME= bundled_plugin.zip
 
 # Tool Versions
 GOTESTSUM_VERSION=v1.8.1
-GOLANGCI_LINT_VERSION=v1.62.0
+GOLANGCI_LINT_VERSION=v1.63.4
 MOCKERY_VERSION=v2.20.0
 YGOT_GENERATOR_VERSION=v0.27.0
 YGOT_GENERATOR_GENERATOR_VERSION=v0.0.4
-- 
GitLab


From d57f811bde015249e6f9fe7803655c4ce6aa5261 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Tue, 14 Jan 2025 08:19:07 +0000
Subject: [PATCH 39/45] [renovate] Update module github.com/golang/glog to
 v1.2.4

See merge request danet/gosdn!1154

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index ef618f305..05dac3b36 100644
--- a/go.mod
+++ b/go.mod
@@ -41,7 +41,7 @@ require (
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt v3.2.2+incompatible
-	github.com/golang/glog v1.2.3
+	github.com/golang/glog v1.2.4
 	github.com/golang/protobuf v1.5.4
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gookit/color v1.5.4 // indirect
diff --git a/go.sum b/go.sum
index fc4bb17f3..8ed1358ae 100644
--- a/go.sum
+++ b/go.sum
@@ -132,6 +132,8 @@ github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
 github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM=
 github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
+github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-- 
GitLab


From 299c784b2ebabc01e26a4029906b0249e8ef57ae Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Tue, 14 Jan 2025 08:29:32 +0000
Subject: [PATCH 40/45] [renovate] Update module google.golang.org/grpc to
 v1.69.4

See merge request danet/gosdn!1155

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 2 +-
 go.sum | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 05dac3b36..084198b91 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@ require (
 	github.com/stretchr/testify v1.10.0
 	go.mongodb.org/mongo-driver v1.17.2
 	golang.org/x/sync v0.10.0
-	google.golang.org/grpc v1.69.2
+	google.golang.org/grpc v1.69.4
 	google.golang.org/protobuf v1.36.2
 	gopkg.in/yaml.v3 v3.0.1
 )
diff --git a/go.sum b/go.sum
index 8ed1358ae..e9fb90e36 100644
--- a/go.sum
+++ b/go.sum
@@ -659,6 +659,8 @@ google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
 google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
 google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
 google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
+google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
+google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-- 
GitLab


From 7289637f33723cb0ef3c5bc7053bb0419b2c90b2 Mon Sep 17 00:00:00 2001
From: Fabian Seidl <fabian.seidl@h-da.de>
Date: Tue, 14 Jan 2025 14:11:30 +0000
Subject: [PATCH 41/45] Resolve "Improve subscription logging"

See merge request danet/gosdn!1156
---
 controller/nucleus/networkElementWatcher.go | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/controller/nucleus/networkElementWatcher.go b/controller/nucleus/networkElementWatcher.go
index 8b542d086..07ad775f2 100644
--- a/controller/nucleus/networkElementWatcher.go
+++ b/controller/nucleus/networkElementWatcher.go
@@ -200,6 +200,8 @@ func (n *NetworkElementWatcher) StopAndRemoveNetworkElementSubscription(subID uu
 // handleSubscribeResponse takes the subscribe response and additional information about the network element to distinguish
 // from which network element a subscribe response was sent including improved error handling.
 func (n *NetworkElementWatcher) handleSubscribeResponse(subscriptionInfo *transport.SubscriptionInformation, workerName string) {
+	log.Debugf("Received Subscribe response: MNE ID: %s, MNE Name: %s, SubResponse: %v", subscriptionInfo.NetworkElementID, subscriptionInfo.NetworkElementName, subscriptionInfo.SubResponse)
+
 	if subscriptionInfo.SubResponse == nil {
 		// Note: This needs proper error handling, no idea how yet. Simply logging would lead to spam in the console
 		// if the target that was subscribed to is not reachable anymore.
@@ -232,6 +234,11 @@ func (n *NetworkElementWatcher) handleSubscribeResponse(subscriptionInfo *transp
 func (n *NetworkElementWatcher) handleSubscribeResponseUpdate(resp *gpb.SubscribeResponse_Update, subscriptionInfo *transport.SubscriptionInformation) {
 	pathsAndValues := make(map[string]string, len(resp.Update.Update))
 
+	if resp.Update == nil || len(resp.Update.Update) == 0 {
+		log.Debugf("handleSubscribeResponseUpdate empty update or updates; Update: %v, InnerUpdates: %v", resp.Update, resp.Update.Update)
+		return
+	}
+
 	for _, update := range resp.Update.Update {
 		pathString, err := ygot.PathToString(update.Path)
 		if err != nil {
-- 
GitLab


From 9839c9bf1f2f12ef40a75a1bc7d3ed2ef1390853 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Tue, 14 Jan 2025 15:53:35 +0000
Subject: [PATCH 42/45] [renovate] Update renovate/renovate Docker tag to
 v39.107.0

See merge request danet/gosdn!1157

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 .gitlab/ci/.renovate.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab/ci/.renovate.yml b/.gitlab/ci/.renovate.yml
index 4d7442638..81070e9da 100644
--- a/.gitlab/ci/.renovate.yml
+++ b/.gitlab/ci/.renovate.yml
@@ -1,7 +1,7 @@
 renovate:
     stage: tools
 
-    image: renovate/renovate:39.106.0
+    image: renovate/renovate:39.107.0
 
     variables:
         LOG_LEVEL: debug
-- 
GitLab


From 7d17bc8826367497aa3dcd87951683f73926af74 Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Wed, 15 Jan 2025 08:22:44 +0000
Subject: [PATCH 43/45] [renovate] Update module
 github.com/bufbuild/protovalidate-go to v0.8.2

See merge request danet/gosdn!1131

Co-authored-by: Fabian Seidl <fabian.seidl@h-da.de>
Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 controller/northbound/server/auth_test.go     | 16 ++++-
 controller/northbound/server/role_test.go     | 71 ++++++++++++++++---
 controller/northbound/server/topology_test.go | 63 ++++++++++++++--
 controller/northbound/server/user_test.go     | 30 +++++++-
 controller/northbound/server/utils_test.go    |  3 +-
 go.mod                                        |  4 +-
 go.sum                                        |  4 ++
 7 files changed, 169 insertions(+), 22 deletions(-)

diff --git a/controller/northbound/server/auth_test.go b/controller/northbound/server/auth_test.go
index e5c9e7389..956ef2be1 100644
--- a/controller/northbound/server/auth_test.go
+++ b/controller/northbound/server/auth_test.go
@@ -89,7 +89,13 @@ func TestAuth_Login(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("username"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("username"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
@@ -160,7 +166,13 @@ func TestAuth_Logout(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("username"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("username"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
diff --git a/controller/northbound/server/role_test.go b/controller/northbound/server/role_test.go
index bb787a416..2d75e0678 100644
--- a/controller/northbound/server/role_test.go
+++ b/controller/northbound/server/role_test.go
@@ -84,10 +84,20 @@ func TestRole_CreateRoles(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("roles[0].name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("roles"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
-				}},
+				},
+			},
 		},
 		{
 			name: "role with too short description should fail",
@@ -105,7 +115,16 @@ func TestRole_CreateRoles(t *testing.T) {
 			want:    &apb.CreateRolesResponse{},
 			wantErr: true,
 			validationErrors: []*validate.Violation{{
-				FieldPath:    stringToPointer("roles[0].description"),
+				Field: &validate.FieldPath{
+					Elements: []*validate.FieldPathElement{
+						{
+							FieldName: stringToPointer("roles"),
+						},
+						{
+							FieldName: stringToPointer("description"),
+						},
+					},
+				},
 				ConstraintId: stringToPointer("string.min_len"),
 				Message:      stringToPointer("value length must be at least 3 characters"),
 			}},
@@ -181,7 +200,13 @@ func TestRole_GetRole(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("role_name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("role_name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
@@ -355,7 +380,16 @@ func TestRole_UpdateRoles(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("roles[0].name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("roles"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
 				},
@@ -379,7 +413,16 @@ func TestRole_UpdateRoles(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("roles[0].description"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("roles"),
+							},
+							{
+								FieldName: stringToPointer("description"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
 				},
@@ -456,12 +499,24 @@ func TestRole_DeletePermissionsForRole(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("role_name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("role_name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("permissions_to_delete"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("permissions_to_delete"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
diff --git a/controller/northbound/server/topology_test.go b/controller/northbound/server/topology_test.go
index 44ce77b57..dea560a45 100644
--- a/controller/northbound/server/topology_test.go
+++ b/controller/northbound/server/topology_test.go
@@ -285,27 +285,72 @@ func TestTopology_AddLink(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("link.name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 1 characters"),
 				},
 				{
-					FieldPath:    stringToPointer("link.sourceNode"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("sourceNode"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("link.targetNode"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("targetNode"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("link.sourcePort"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("sourcePort"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("link.targetPort"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("targetPort"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
@@ -461,7 +506,13 @@ func TestTopology_DeleteLink(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("id"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("id"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
diff --git a/controller/northbound/server/user_test.go b/controller/northbound/server/user_test.go
index 4a1c66327..9fcdf4d61 100644
--- a/controller/northbound/server/user_test.go
+++ b/controller/northbound/server/user_test.go
@@ -90,7 +90,16 @@ func TestUser_CreateUsers(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("user[0].password"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("user"),
+							},
+							{
+								FieldName: stringToPointer("password"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 5 characters"),
 				}},
@@ -116,7 +125,16 @@ func TestUser_CreateUsers(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("user[0].name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("user"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
 				}},
@@ -190,7 +208,13 @@ func TestUser_GetUser(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
diff --git a/controller/northbound/server/utils_test.go b/controller/northbound/server/utils_test.go
index 7a4636caa..dd95d8f2e 100644
--- a/controller/northbound/server/utils_test.go
+++ b/controller/northbound/server/utils_test.go
@@ -23,7 +23,8 @@ func isEqualFieldPaths(violationFieldPath, errFieldPath *validate.FieldPath) boo
 	}
 
 	for i, elem := range violationFieldPath.GetElements() {
-		if elem != errFieldPath.GetElements()[i] {
+		errElem := errFieldPath.GetElements()[i]
+		if *elem.FieldName != *errElem.FieldName {
 			return false
 		}
 	}
diff --git a/go.mod b/go.mod
index 084198b91..9cbe7dabc 100644
--- a/go.mod
+++ b/go.mod
@@ -87,7 +87,7 @@ require (
 
 require (
 	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20241127180247-a33202765966.1
-	github.com/bufbuild/protovalidate-go v0.7.3
+	github.com/bufbuild/protovalidate-go v0.8.2
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.4.10
 	github.com/lesismal/nbio v1.5.12
@@ -103,7 +103,7 @@ require (
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/containerd/console v1.0.3 // indirect
 	github.com/fatih/color v1.15.0 // indirect
-	github.com/google/cel-go v0.22.0 // indirect
+	github.com/google/cel-go v0.22.1 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-hclog v1.5.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
diff --git a/go.sum b/go.sum
index e9fb90e36..7a355f78a 100644
--- a/go.sum
+++ b/go.sum
@@ -79,6 +79,8 @@ github.com/bufbuild/protovalidate-go v0.7.2 h1:UuvKyZHl5p7u3ztEjtRtqtDxOjRKX5VUO
 github.com/bufbuild/protovalidate-go v0.7.2/go.mod h1:PHV5pFuWlRzdDW02/cmVyNzdiQ+RNNwo7idGxdzS7o4=
 github.com/bufbuild/protovalidate-go v0.7.3 h1:kKnoSueygR3xxppvuBpm9SEwIsP359MMRfMBGmRByPg=
 github.com/bufbuild/protovalidate-go v0.7.3/go.mod h1:CFv34wMqiBzAHdQ4q/tWYi9ILFYKuaC3/4zh6eqdUck=
+github.com/bufbuild/protovalidate-go v0.8.2 h1:sgzXHkHYP6HnAsL2Rd3I1JxkYUyEQUv9awU1PduMxbM=
+github.com/bufbuild/protovalidate-go v0.8.2/go.mod h1:K6w8iPNAXBoIivVueSELbUeUl+MmeTQfCDSug85pn3M=
 github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI=
 github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@@ -156,6 +158,8 @@ github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
 github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
 github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g=
 github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
+github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40=
+github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-- 
GitLab


From 6706a91a76994e8fdbc7551b4279fe74f6fef99a Mon Sep 17 00:00:00 2001
From: renovate_bot
 <group_8045_bot_08826d7c233c44435d2dae5013b96892@noreply.code.fbi.h-da.de>
Date: Wed, 15 Jan 2025 14:41:45 +0000
Subject: [PATCH 44/45] [renovate] Update module go.mongodb.org/mongo-driver to
 v2

See merge request danet/gosdn!1129

Co-authored-by: Renovate Bot <renovate@danet.fbi.h-da.de>
---
 go.mod | 3 ++-
 go.sum | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 9cbe7dabc..f48265086 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/spf13/viper v1.19.0
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/stretchr/testify v1.10.0
-	go.mongodb.org/mongo-driver v1.17.2
+	go.mongodb.org/mongo-driver/v2 v2.0.0
 	golang.org/x/sync v0.10.0
 	google.golang.org/grpc v1.69.4
 	google.golang.org/protobuf v1.36.2
@@ -91,6 +91,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.4.10
 	github.com/lesismal/nbio v1.5.12
+	go.mongodb.org/mongo-driver v1.17.2
 	google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422
 )
 
diff --git a/go.sum b/go.sum
index 7a355f78a..2fc1f0098 100644
--- a/go.sum
+++ b/go.sum
@@ -414,6 +414,7 @@ go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHy
 go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
 go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
 go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-- 
GitLab


From e44cc99e109850a1157f2b4816d63a1b089d2f80 Mon Sep 17 00:00:00 2001
From: Matthias Feyll <matthias.feyll@stud.h-da.de>
Date: Thu, 16 Jan 2025 01:00:04 +0100
Subject: [PATCH 45/45] (ui): implement UpdateIndicator

---
 .../devices/reducer/device.reducer.ts         | 15 ++++++
 .../devices/routines/device.routine.ts        | 12 ++++-
 .../components/devices/view/device.view.tsx   | 14 +++--
 .../devices/view_model/device.viewmodel.ts    |  2 +-
 .../src/i18n/locales/en/translations.json     |  3 ++
 .../update-indicator.layout.tsx               | 52 +++++++++++++++++++
 .../update-indicator.viewmodel.tsx            | 39 ++++++++++++++
 .../src/shared/reducer/routine.reducer.ts     | 10 +++-
 react-ui/src/shared/style/colors.scss         |  2 +-
 react-ui/src/shared/types/thunk.type.ts       |  3 +-
 10 files changed, 142 insertions(+), 10 deletions(-)
 create mode 100644 react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.layout.tsx
 create mode 100644 react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.viewmodel.tsx

diff --git a/react-ui/src/components/devices/reducer/device.reducer.ts b/react-ui/src/components/devices/reducer/device.reducer.ts
index cea12fbc9..f211fe024 100755
--- a/react-ui/src/components/devices/reducer/device.reducer.ts
+++ b/react-ui/src/components/devices/reducer/device.reducer.ts
@@ -5,6 +5,8 @@ import {
 } from '@api/api'
 import { DeviceViewTabValues } from '@component/devices/view/device.view.tabs'
 import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { refreshUpdateTimer } from '@shared/reducer/routine.reducer'
+import { Category, CategoryType } from '@shared/types/category.type'
 import { REHYDRATE } from 'redux-persist'
 import { RootState } from 'src/stores'
 import '../routines/index'
@@ -129,6 +131,19 @@ startListening({
     },
 })
 
+startListening({
+    predicate: (action) => setSelectedMne.match(action),
+    effect: async (action, listenerApi) => {
+        listenerApi.dispatch(refreshUpdateTimer(Category.TAB as CategoryType))
+    },
+})
+
+startListening({
+    predicate: (action) => setDevices.match(action),
+    effect: async (action, listenerApi) => {
+        listenerApi.dispatch(refreshUpdateTimer(Category.DEVICE as CategoryType))
+    },
+})
 
 /**
  * On startup reset the selected device 
diff --git a/react-ui/src/components/devices/routines/device.routine.ts b/react-ui/src/components/devices/routines/device.routine.ts
index ef92b1c8e..058f65f9f 100755
--- a/react-ui/src/components/devices/routines/device.routine.ts
+++ b/react-ui/src/components/devices/routines/device.routine.ts
@@ -1,21 +1,29 @@
 import { NetworkElementServiceGetAllFlattenedApiArg, api } from '@api/api'
 import { setDevices } from '@component/devices/reducer/device.reducer'
 import { createAsyncThunk } from '@reduxjs/toolkit'
+import { addRoutine } from '@shared/reducer/routine.reducer'
 import { setUser } from '@shared/reducer/user.reducer'
+import { Category, CategoryType } from '@shared/types/category.type'
 import { RootState } from 'src/stores'
 import { startListening } from '../../../stores/middleware/listener.middleware'
 
 export const FETCH_DEVICE_ACTION = 'subscription/device/fetchDevices'
 
 // continously fetch devices
-const FETCH_DEVICES_INTERVAL = 15000 // in ms
 startListening({
     actionCreator: setUser,
     effect: async (_, listenerApi) => {
-        listenerApi.dispatch(fetchDevicesThunk())
+        listenerApi.dispatch(
+            addRoutine({
+                thunk: fetchDevicesThunk,
+                category: Category.DEVICE as CategoryType,
+                payload: {},
+            })
+        )
     },
 })
 
+const FETCH_DEVICES_INTERVAL = 15000 // in ms
 export const fetchDevicesThunk = createAsyncThunk(FETCH_DEVICE_ACTION, (_, thunkApi) => {
     const { user } = thunkApi.getState() as RootState
 
diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx
index f705aa4eb..6bd702bf7 100755
--- a/react-ui/src/components/devices/view/device.view.tsx
+++ b/react-ui/src/components/devices/view/device.view.tsx
@@ -1,13 +1,15 @@
 import { faGripVertical } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { GridLayout } from '@layout/grid.layout/grid.layout';
+import UpdateIndicator from '@layout/grid.layout/update-inidicator.layout/update-indicator.layout';
+import { Category, CategoryType } from '@shared/types/category.type';
 import { useRef } from 'react';
 import { Button, Col, Container, Form, Nav, NavLink, Row } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 import { useDeviceViewModel } from '../view_model/device.viewmodel';
 import './device.scss';
 import { DeviceViewTable } from './device.view.table';
-import { DeviceViewTabs, DeviceViewTabValues } from './device.view.tabs';
+import { DeviceViewTabValues, DeviceViewTabs } from './device.view.tabs';
 
 const DeviceView = () => {
     const { t } = useTranslation('common');
@@ -20,13 +22,16 @@ const DeviceView = () => {
                 <>
                     <div key="device-list">
                         <Container className='c-box hoverable h-100'>
+                            <UpdateIndicator
+                                category={Category.DEVICE as CategoryType}
+                                updateInterval={15000}
+                            />
                             <FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
                             <Row>
                                 <Col sm={12} className='mt-4'>
                                     <h3 className='text-black-50'>{t('device.title')}</h3>
                                 </Col>
                             </Row>
-
                             <Row className='align-items-center'>
                                 <Col xs={12} sm={6}>
                                     <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'>
@@ -48,6 +53,10 @@ const DeviceView = () => {
 
                     <div key="device-details">
                         <Container className='c-box hoverable h-100'>
+                            <UpdateIndicator
+                                category={Category.TAB as CategoryType}
+                                updateInterval={5000}
+                            />
                             <FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
                             <Row>
                                 <Col xs={12} className='mt-4'>
@@ -67,7 +76,6 @@ const DeviceView = () => {
                                     </Nav>
                                 </Col>
                             </Row>
-
                             <Row className='align-items-start'>
                                 <Col xs={12}>
                                     {DeviceViewTabs(activeTab)}
diff --git a/react-ui/src/components/devices/view_model/device.viewmodel.ts b/react-ui/src/components/devices/view_model/device.viewmodel.ts
index 9a0fbe17a..1cce2d59a 100755
--- a/react-ui/src/components/devices/view_model/device.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.viewmodel.ts
@@ -7,7 +7,7 @@ export const useDeviceViewModel = () => {
     const { activeTab } = useAppSelector((state) => state.device)
     const dispatch = useAppDispatch()
 
-    useEffect(() => {}, [])
+    useEffect(() => { }, [])
 
     const handleActiveTabLink = (tabLink: DeviceViewTabValues) => {
         return activeTab === tabLink ? 'active' : ''
diff --git a/react-ui/src/i18n/locales/en/translations.json b/react-ui/src/i18n/locales/en/translations.json
index fb3ca729c..53444b9e0 100755
--- a/react-ui/src/i18n/locales/en/translations.json
+++ b/react-ui/src/i18n/locales/en/translations.json
@@ -53,6 +53,9 @@
                 "yang_model": {
                     "title": "YANG Model"
                 }
+            },
+            "box": {
+                "lastUpdate": "Last updated {{seconds}} seconds ago"
             }
         },
         "protected": {
diff --git a/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.layout.tsx b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.layout.tsx
new file mode 100644
index 000000000..d71bb6cce
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.layout.tsx
@@ -0,0 +1,52 @@
+import { faCircle } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React, { useState } from 'react'
+import { Overlay, Tooltip } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import { CategoryType } from '../types'
+import { useUpdateIndicatorViewModel } from './update-indicator.viewmodel'
+
+interface UpdateIndicatorProps {
+    category: CategoryType
+    updateInterval: number
+}
+
+const UpdateIndicator: React.FC<UpdateIndicatorProps> = ({ category, updateInterval }) => {
+    const [showTooltip, setShowTooltip] = useState(false)
+    const { t } = useTranslation('common')
+    const target = React.useRef(null)
+    const { secondsSinceUpdate, getStatusColor } = useUpdateIndicatorViewModel(category)
+
+    return (
+        <div
+            className="position-absolute"
+            style={{
+                top: 0,
+                right: '40px',
+                padding: '10px',
+                zIndex: 10
+            }}
+        >
+            <div
+                ref={target}
+                onMouseEnter={() => setShowTooltip(true)}
+                onMouseLeave={() => setShowTooltip(false)}
+                style={{ cursor: 'pointer' }}
+            >
+                <FontAwesomeIcon
+                    icon={faCircle}
+                    className={getStatusColor(updateInterval)}
+                    size="sm"
+                />
+            </div>
+
+            <Overlay target={target.current} show={showTooltip} placement="bottom">
+                <Tooltip id="update-tooltip">
+                    {t('device.box.lastUpdate', { seconds: secondsSinceUpdate })}
+                </Tooltip>
+            </Overlay>
+        </div>
+    )
+}
+
+export default UpdateIndicator
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.viewmodel.tsx b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.viewmodel.tsx
new file mode 100644
index 000000000..bb91b0b17
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.viewmodel.tsx
@@ -0,0 +1,39 @@
+import { useAppSelector } from "@hooks"
+import { CategoryType } from "@shared/types/category.type"
+import { useEffect, useState } from 'react'
+
+export const useUpdateIndicatorViewModel = (category: CategoryType) => {
+    const { thunks } = useAppSelector((state) => state.routine)
+    const [secondsSinceUpdate, setSecondsSinceUpdate] = useState<number>(-1)
+
+    useEffect(() => {
+        const updateTimer = () => {
+            const lastupdate = thunks[category]?.lastupdate
+            if (lastupdate) {
+                setSecondsSinceUpdate(Math.round((Date.now() - lastupdate) / 1000))
+            } else {
+                setSecondsSinceUpdate(-1)
+            }
+        }
+
+        // Initial update
+        updateTimer()
+
+        // Set up interval for updates
+        const intervalId = setInterval(updateTimer, 1000)
+
+        return () => clearInterval(intervalId)
+    }, [category, thunks])
+
+    const getStatusColor = (updateInterval: number) => {
+        const updateIntervalSeconds = updateInterval / 1000
+        if (secondsSinceUpdate > updateIntervalSeconds * 0.9) return "text-primary"
+        if (secondsSinceUpdate > updateIntervalSeconds * 1.3) return "text-danger"
+        return "text-bg-primary"
+    }
+
+    return {
+        secondsSinceUpdate,
+        getStatusColor
+    }
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/reducer/routine.reducer.ts b/react-ui/src/shared/reducer/routine.reducer.ts
index 5e9c3401a..95d5f06e7 100755
--- a/react-ui/src/shared/reducer/routine.reducer.ts
+++ b/react-ui/src/shared/reducer/routine.reducer.ts
@@ -16,6 +16,7 @@ const initialState: ReducerState = {
         TABLE: null,
         TAB: null
     },
+
 }
 
 
@@ -27,19 +28,24 @@ const RoutineSlice = createSlice({
             const thunk: ThunkPersist = {
                 category: payload.category,
                 payload: payload.payload,
-                thunkId: payload.thunk.id
+                thunkId: payload.thunk.id,
+                lastupdate: Date.now()
             }
 
             state.thunks[payload.category] = thunk
         },
 
+        refreshUpdateTimer: (state: any, { payload }: PayloadAction<CategoryType>) => {
+            state.thunks[payload].lastupdate = Date.now()
+        },
+
         removeAll: (state) => {
             state.thunks = initialState.thunks
         },
     },
 })
 
-export const { addRoutine } = RoutineSlice.actions
+export const { addRoutine, refreshUpdateTimer } = RoutineSlice.actions
 
 // on logout remove all routine
 startListening({
diff --git a/react-ui/src/shared/style/colors.scss b/react-ui/src/shared/style/colors.scss
index 29c971f86..4469a4a5b 100755
--- a/react-ui/src/shared/style/colors.scss
+++ b/react-ui/src/shared/style/colors.scss
@@ -2,7 +2,7 @@ $theme-colors: (
   "primary": #b350e0,
   "primary::hover": #ddaff3af,
   "bg-primary": #ededed,
-  "danger": #ffdcdc,
+  "danger": #ff0000,
   "warning": #dbd116,
   "dark": #595959,
   "black": #000000
diff --git a/react-ui/src/shared/types/thunk.type.ts b/react-ui/src/shared/types/thunk.type.ts
index 9143871f0..ff037d796 100644
--- a/react-ui/src/shared/types/thunk.type.ts
+++ b/react-ui/src/shared/types/thunk.type.ts
@@ -19,5 +19,6 @@ export interface ThunkDTO {
 export interface ThunkPersist {
     thunkId: number,
     payload: Object
-    category: CategoryType
+    category: CategoryType,
+    lastupdate: number
 }
-- 
GitLab