Repository: kiali/kiali
Pull request #7575 (2024-07-31 11:57:18)

Add badge icon to waypoints
### Describe the change

Add badge icon to waypoints in the Traffic graph: 

![image](https://github.com/user-attachments/assets/1dde0a9c-93d9-4fdf-9861-8beb5d0ba23d)

- Included a legend (Just in Ambient) 

Added to PF graph also: 

![image](https://github.com/user-attachments/assets/767a271e-3024-4c72-9471-6c7910d78e45)


### Steps to test the PR

Install Kiali and Ambient: 

- `minikube start`
- Install istio with Ambient profile: `istio/install-istio-via-istioctl.sh -c kubectl -cp ambient`
- Install kiali
- Install bookinfo (ztunnel and waypoint): `istio/install-bookinfo-demo.sh -c kubectl -ai false -tg -w true`

### Automation testing

e2e tests will be added in a follow up PR

### Issue reference
https://github.com/kiali/kiali/issues/7445 



Patch:
diff --git a/config/config.go b/config/config.go
index 8d80c8046..002d89b55 100644
--- a/config/config.go
+++ b/config/config.go
@@ -968,6 +968,11 @@ func (conf *Config) GatewayLabel(labelConfig string) []string {
 	return []string{}
 }
 
+func (conf *Config) IsRBACDisabled() bool {
+	return conf.Auth.Strategy == AuthStrategyAnonymous ||
+		(conf.Auth.Strategy == AuthStrategyOpenId && conf.Auth.OpenId.DisableRBAC)
+}
+
 // Get the global Config
 func Get() (conf *Config) {
 	rwMutex.RLock()
diff --git a/frontend/package.json b/frontend/package.json
index 4563675bc..29411bebe 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -145,7 +145,7 @@
     "enzyme": "3.11.0",
     "enzyme-to-json": "3.4.4",
     "husky": "^8.0.3",
-    "i18next-parser": "^8.12.0",
+    "i18next-parser": "^9.0.1",
     "jest-canvas-mock": "2.2.0",
     "junit-report-merger": "^3.0.5",
     "mocha-junit-reporter": "^2.2.1",
@@ -164,6 +164,7 @@
     "@babel/plugin-transform-react-jsx": "^7.22.15",
     "async": "^3.2.2",
     "json-schema": "^0.4.0",
+    "micromatch": "^4.0.7",
     "minimist": "^1.2.6",
     "nth-check": "^2.0.1",
     "postcss": "^8.4.31",
diff --git a/frontend/src/assets/img/graph/node-badge-waypoint.svg b/frontend/src/assets/img/graph/node-badge-waypoint.svg
new file mode 100644
index 000000000..1cddf7044
--- /dev/null
+++ b/frontend/src/assets/img/graph/node-badge-waypoint.svg
@@ -0,0 +1,2 @@
+﻿<?xml version="1.0" standalone="no"?>
+<svg fill="#fff" height="27" width="27" viewBox="0 0 1424 1424" aria-hidden="true" role="img" style="vertical-align: -0.125em;margin: 4px" aria-describedby="pf-tooltip-2974" xmlns="http://www.w3.org/2000/svg"><path d="M592,64 C618.509668,64 640,85.490332 640,112 L640,112 L640,272 C640,272.7 639.9,273.5 639.9,274.3 L639.9,274.3 L749.6,384 L864,384 C881.673112,384 896,398.326888 896,416 L896,416 L896,544 C896,561.673112 881.673112,576 864,576 L864,576 L832,576 L832,639 L912,639 C938.509668,639 960,660.490332 960,687 L960,687 L960,768 L992,768 C1009.67311,768 1024,782.326888 1024,800 L1024,800 L1024,928 C1024,945.673112 1009.67311,960 992,960 L992,960 L864,960 C846.326888,960 832,945.673112 832,928 L832,928 L832,800 C832,782.326888 846.326888,768 864,768 L864,768 L896,768 L896,703 L704,703 L704,768 L736,768 C753.673112,768 768,782.326888 768,800 L768,800 L768,928 C768,945.673112 753.673112,960 736,960 L736,960 L608,960 C590.326888,960 576,945.673112 576,928 L576,928 L576,800 C576,782.326888 590.326888,768 608,768 L608,768 L640,768 L640,687 C640,660.490332 661.490332,639 688,639 L688,639 L768,639 L768,576 L736,576 C718.326888,576 704,561.673112 704,544 L704,544 L704,428.9 L595,319.9 C594,320 593,320 592,320 L592,320 L430.3,320 L320,430.3 L320,544 C320,561.673112 305.673112,576 288,576 L288,576 L256,576 L256,639 L336,639 C362.509668,639 384,660.490332 384,687 L384,687 L384,768 L416,768 C433.673112,768 448,782.326888 448,800 L448,800 L448,928 C448,945.673112 433.673112,960 416,960 L416,960 L288,960 C270.326888,960 256,945.673112 256,928 L256,928 L256,800 C256,782.326888 270.326888,768 288,768 L288,768 L320,768 L320,703 L128,703 L128,768 L160,768 C177.673112,768 192,782.326888 192,800 L192,800 L192,928 C192,945.673112 177.673112,960 160,960 L160,960 L32,960 C14.326888,960 0,945.673112 0,928 L0,928 L0,800 C0,782.326888 14.326888,768 32,768 L32,768 L64,768 L64,687 C64,660.490332 85.490332,639 112,639 L112,639 L192,639 L192,576 L160,576 C142.326888,576 128,561.673112 128,544 L128,544 L128,416 C128,398.326888 142.326888,384 160,384 L160,384 L275.7,384 L384.1,275.6 C384.1,274.4 384,273.2 384,272 L384,272 L384,112 C384,85.490332 405.490332,64 432,64 L432,64 Z M576,128 L448,128 L448,256 L576,256 L576,128 Z"></path></svg>
diff --git a/frontend/src/components/CytoscapeGraph/graphs/GraphStyles.ts b/frontend/src/components/CytoscapeGraph/graphs/GraphStyles.ts
index bf4a8de83..29cb30f53 100644
--- a/frontend/src/components/CytoscapeGraph/graphs/GraphStyles.ts
+++ b/frontend/src/components/CytoscapeGraph/graphs/GraphStyles.ts
@@ -70,6 +70,7 @@ const NodeIconRequestRouting = icons.istio.requestRouting.className; // code-bra
 const NodeIconRequestTimeout = icons.istio.requestTimeout.className; // clock
 const NodeIconTrafficShifting = icons.istio.trafficShifting.className; // share-alt
 const NodeIconWorkloadEntry = icons.istio.workloadEntry.className; // pf-icon-virtual-machine
+const NodeIconWaypoint = icons.istio.waypoint.className;
 const NodeTextColor = PFColors.Black1000;
 const NodeTextColorBox = PFColors.Black1000;
 const NodeTextBackgroundColor = PFColors.White;
@@ -240,6 +241,7 @@ export class GraphStyles {
     // const isClusterBoxed = box1Type === BoxByType.CLUSTER || box2Type === BoxByType.CLUSTER || box3Type === BoxByType.CLUSTER;
     const isMultiNamespace = cyGlobal.activeNamespaces.length > 1;
     const isOutside = node.isOutside;
+    const isWaypoint = node.isWaypoint || '';
 
     // Badges portion of label...
 
@@ -300,6 +302,9 @@ export class GraphStyles {
         if (node.isGateway?.egressInfo?.hostnames?.length !== undefined) {
           badges = `<span class="${NodeIconGateway} ${badgeMargin(badges)}"></span> ${badges}`;
         }
+        if (isWaypoint) {
+          badges = `<span class="${NodeIconWaypoint} ${badgeMargin(badges)}"></span> ${badges}`;
+        }
       }
     }
 
diff --git a/frontend/src/config/Icons.ts b/frontend/src/config/Icons.ts
index 77f32e4ea..5da869de0 100644
--- a/frontend/src/config/Icons.ts
+++ b/frontend/src/config/Icons.ts
@@ -8,6 +8,7 @@ import {
   ClockIcon,
   CodeBranchIcon,
   GlobeRouteIcon,
+  InfrastructureIcon,
   LockedIcon,
   MigrationIcon,
   SecurityIcon,
@@ -143,7 +144,15 @@ const mutIcons = {
       name: 'virtual-machine',
       text: 'Workload Entry',
       type: 'pf'
-    } as IconType
+    } as IconType,
+    waypoint: {
+      ascii: 'E93D ',
+      className: 'pf-v5-pficon pf-v5-pficon-infrastructure',
+      icon: InfrastructureIcon,
+      name: 'infrastructure',
+      text: 'Waypoint Proxy',
+      type: 'pf'
+    }
   }
 };
 
diff --git a/frontend/src/pages/Graph/GraphLegend.tsx b/frontend/src/pages/Graph/GraphLegend.tsx
index 8d62735b3..077153d41 100644
--- a/frontend/src/pages/Graph/GraphLegend.tsx
+++ b/frontend/src/pages/Graph/GraphLegend.tsx
@@ -100,7 +100,7 @@ export const GraphLegend: React.FC<GraphLegendProps> = (props: GraphLegendProps)
         </Tooltip>
       </div>
 
-      <div className={bodyStyle}>{renderGraphLegendList(legendData)}</div>
+      <div className={bodyStyle}>{renderGraphLegendList(legendData())}</div>
     </div>
   );
 };
diff --git a/frontend/src/pages/Graph/GraphLegendData.ts b/frontend/src/pages/Graph/GraphLegendData.ts
index 00856cbb5..55bb61b22 100644
--- a/frontend/src/pages/Graph/GraphLegendData.ts
+++ b/frontend/src/pages/Graph/GraphLegendData.ts
@@ -34,7 +34,9 @@ import badgeTrafficShiftingSourceImage from '../../assets/img/graph/node-badge-t
 import badgeTrafficSourceImage from '../../assets/img/graph/node-badge-traffic-source.svg';
 import badgeVirtualServicesImage from '../../assets/img/graph/node-badge-virtual-services.svg';
 import badgeWorkloadEntryImage from '../../assets/img/graph/node-badge-workload-entry.svg';
+import badgeWaypointImage from '../../assets/img/graph/node-badge-waypoint.svg';
 import { t } from 'utils/I18nUtils';
+import { serverConfig } from '../../config';
 
 export interface GraphLegendItem {
   data: GraphLegendItemRow[];
@@ -47,66 +49,74 @@ export interface GraphLegendItemRow {
   label: string;
 }
 
-export const legendData: GraphLegendItem[] = [
-  {
-    title: t('Node Shapes'),
-    data: [
-      { label: t('Workload'), icon: workloadImage },
-      { label: t('App'), icon: appImage },
-      { label: t('Operation'), icon: operationImage },
-      { label: t('Service'), icon: serviceImage },
-      { label: t('Service Entry'), icon: serviceEntryImage }
-    ]
-  },
-  {
-    title: t('Node Colors'),
-    data: [
-      { label: t('Normal'), icon: nodeColorNormalImage },
-      { label: t('Warn'), icon: nodeColorWarningImage },
-      { label: t('Unhealthy'), icon: nodeColorDangerImage },
-      { label: t('Idle'), icon: nodeColorIdleImage }
-    ]
-  },
-  {
-    title: t('Node Background'),
-    data: [
-      { label: t('Unselected Namespace'), icon: externalNamespaceImage },
-      { label: t('Restricted / External'), icon: restrictedNamespaceImage }
-    ]
-  },
-  {
-    title: t('Edges'),
-    data: [
-      { label: t('Failure'), icon: edgeDangerImage },
-      { label: t('Degraded'), icon: edgeWarnImage },
-      { label: t('Healthy'), icon: edgeSuccessImage },
-      { label: t('TCP Connection'), icon: edgeTcpImage },
-      { label: t('Idle'), icon: edgeIdlemage },
-      { label: t('mTLS (badge)'), icon: edgeMtlsImage }
-    ]
-  },
-  {
-    title: t('Traffic Animation'),
-    data: [
-      { label: t('Normal Request'), icon: trafficNormalImage },
-      { label: t('Failed Request'), icon: trafficFailedImage },
-      { label: t('TCP Traffic'), icon: trafficTcpImage }
-    ]
-  },
-  {
-    title: t('Node Badges'),
-    isBadge: true,
-    data: [
-      { label: t('Circuit Breaker'), icon: badgeCircuitBreakerImage },
-      { label: t('Fault Injection'), icon: badgeFaultInjectionImage },
-      { label: t('Gateway'), icon: badgeGatewaysImage },
-      { label: t('Mirroring'), icon: badgeMirroringImage },
-      { label: t('Missing Sidecar'), icon: badgeMissingSidecarImage },
-      { label: t('Request Timeout'), icon: badgeRequestTimeoutImage },
-      { label: t('Traffic Shifting / TCP Traffic Shifting'), icon: badgeTrafficShiftingSourceImage },
-      { label: t('Traffic Source'), icon: badgeTrafficSourceImage },
-      { label: t('Virtual Service / Request Routing'), icon: badgeVirtualServicesImage },
-      { label: t('Workload Entry'), icon: badgeWorkloadEntryImage }
-    ]
+export const legendData = (): GraphLegendItem[] => {
+  const nodesBadges = [
+    { label: t('Circuit Breaker'), icon: badgeCircuitBreakerImage },
+    { label: t('Fault Injection'), icon: badgeFaultInjectionImage },
+    { label: t('Gateway'), icon: badgeGatewaysImage },
+    { label: t('Mirroring'), icon: badgeMirroringImage },
+    { label: t('Missing Sidecar'), icon: badgeMissingSidecarImage },
+    { label: t('Request Timeout'), icon: badgeRequestTimeoutImage },
+    { label: t('Traffic Shifting / TCP Traffic Shifting'), icon: badgeTrafficShiftingSourceImage },
+    { label: t('Traffic Source'), icon: badgeTrafficSourceImage },
+    { label: t('Virtual Service / Request Routing'), icon: badgeVirtualServicesImage },
+    { label: t('Workload Entry'), icon: badgeWorkloadEntryImage }
+  ];
+
+  if (serverConfig.ambientEnabled) {
+    nodesBadges.push({ label: t('Waypoint'), icon: badgeWaypointImage });
   }
-];
+
+  return [
+    {
+      title: t('Node Shapes'),
+      data: [
+        { label: t('Workload'), icon: workloadImage },
+        { label: t('App'), icon: appImage },
+        { label: t('Operation'), icon: operationImage },
+        { label: t('Service'), icon: serviceImage },
+        { label: t('Service Entry'), icon: serviceEntryImage }
+      ]
+    },
+    {
+      title: t('Node Colors'),
+      data: [
+        { label: t('Normal'), icon: nodeColorNormalImage },
+        { label: t('Warn'), icon: nodeColorWarningImage },
+        { label: t('Unhealthy'), icon: nodeColorDangerImage },
+        { label: t('Idle'), icon: nodeColorIdleImage }
+      ]
+    },
+    {
+      title: t('Node Background'),
+      data: [
+        { label: t('Unselected Namespace'), icon: externalNamespaceImage },
+        { label: t('Restricted / External'), icon: restrictedNamespaceImage }
+      ]
+    },
+    {
+      title: t('Edges'),
+      data: [
+        { label: t('Failure'), icon: edgeDangerImage },
+        { label: t('Degraded'), icon: edgeWarnImage },
+        { label: t('Healthy'), icon: edgeSuccessImage },
+        { label: t('TCP Connection'), icon: edgeTcpImage },
+        { label: t('Idle'), icon: edgeIdlemage },
+        { label: t('mTLS (badge)'), icon: edgeMtlsImage }
+      ]
+    },
+    {
+      title: t('Traffic Animation'),
+      data: [
+        { label: t('Normal Request'), icon: trafficNormalImage },
+        { label: t('Failed Request'), icon: trafficFailedImage },
+        { label: t('TCP Traffic'), icon: trafficTcpImage }
+      ]
+    },
+    {
+      title: t('Node Badges'),
+      isBadge: true,
+      data: nodesBadges
+    }
+  ];
+};
diff --git a/frontend/src/pages/GraphPF/GraphLegendDataPF.ts b/frontend/src/pages/GraphPF/GraphLegendDataPF.ts
index 973819ee5..5d93c8a2f 100644
--- a/frontend/src/pages/GraphPF/GraphLegendDataPF.ts
+++ b/frontend/src/pages/GraphPF/GraphLegendDataPF.ts
@@ -34,7 +34,9 @@ import badgeTrafficShiftingSourceImage from '../../assets/img/graph/node-badge-t
 import badgeTrafficSourceImage from '../../assets/img/graph/node-badge-traffic-source.svg';
 import badgeVirtualServicesImage from '../../assets/img/graph/node-badge-virtual-services.svg';
 import badgeWorkloadEntryImage from '../../assets/img/graph/node-badge-workload-entry.svg';
+import badgeWaypointImage from '../../assets/img/graph/node-badge-waypoint.svg';
 import { t } from 'utils/I18nUtils';
+import { serverConfig } from '../../config';
 
 export interface GraphLegendItem {
   data: GraphLegendItemRow[];
@@ -47,66 +49,74 @@ export interface GraphLegendItemRow {
   label: string;
 }
 
-export const legendData: GraphLegendItem[] = [
-  {
-    title: t('Node Shapes'),
-    data: [
-      { label: t('Workload'), icon: workloadImage },
-      { label: t('App'), icon: appImage },
-      { label: t('Operation'), icon: operationImage },
-      { label: t('Service'), icon: serviceImage },
-      { label: t('Service Entry'), icon: serviceEntryImage }
-    ]
-  },
-  {
-    title: t('Node Colors'),
-    data: [
-      { label: t('Healthy'), icon: nodeColorHealthyImage },
-      { label: t('Warn'), icon: nodeColorWarningImage },
-      { label: t('Unhealthy'), icon: nodeColorDangerImage },
-      { label: t('Idle'), icon: nodeColorIdleImage }
-    ]
-  },
-  {
-    title: t('Node Background'),
-    data: [
-      { label: t('Unselected Namespace'), icon: externalNamespaceImage },
-      { label: t('Restricted / External'), icon: restrictedNamespaceImage }
-    ]
-  },
-  {
-    title: t('Edges'),
-    data: [
-      { label: t('Failure'), icon: edgeDangerImage },
-      { label: t('Degraded'), icon: edgeWarnImage },
-      { label: t('Healthy'), icon: edgeSuccessImage },
-      { label: t('TCP Connection'), icon: edgeTcpImage },
-      { label: t('Idle'), icon: edgeIdlemage },
-      { label: t('mTLS (badge)'), icon: edgeMtlsImage }
-    ]
-  },
-  {
-    title: t('Traffic Animation'),
-    data: [
-      { label: t('Healthy Request'), icon: trafficHealthyImage },
-      { label: t('Failed Request'), icon: trafficFailedImage },
-      { label: t('TCP Traffic'), icon: trafficTcpImage }
-    ]
-  },
-  {
-    title: t('Node Badges'),
-    isBadge: true,
-    data: [
-      { label: t('Circuit Breaker'), icon: badgeCircuitBreakerImage },
-      { label: t('Fault Injection'), icon: badgeFaultInjectionImage },
-      { label: t('Gateway'), icon: badgeGatewaysImage },
-      { label: t('Mirroring'), icon: badgeMirroringImage },
-      { label: t('Missing Sidecar'), icon: badgeMissingSidecarImage },
-      { label: t('Request Timeout'), icon: badgeRequestTimeoutImage },
-      { label: t('Traffic Shifting / TCP Traffic Shifting'), icon: badgeTrafficShiftingSourceImage },
-      { label: t('Traffic Source'), icon: badgeTrafficSourceImage },
-      { label: t('Virtual Service / Request Routing'), icon: badgeVirtualServicesImage },
-      { label: t('Workload Entry'), icon: badgeWorkloadEntryImage }
-    ]
+export const legendData = (): GraphLegendItem[] => {
+  const nodeBadges = [
+    { label: t('Circuit Breaker'), icon: badgeCircuitBreakerImage },
+    { label: t('Fault Injection'), icon: badgeFaultInjectionImage },
+    { label: t('Gateway'), icon: badgeGatewaysImage },
+    { label: t('Mirroring'), icon: badgeMirroringImage },
+    { label: t('Missing Sidecar'), icon: badgeMissingSidecarImage },
+    { label: t('Request Timeout'), icon: badgeRequestTimeoutImage },
+    { label: t('Traffic Shifting / TCP Traffic Shifting'), icon: badgeTrafficShiftingSourceImage },
+    { label: t('Traffic Source'), icon: badgeTrafficSourceImage },
+    { label: t('Virtual Service / Request Routing'), icon: badgeVirtualServicesImage },
+    { label: t('Workload Entry'), icon: badgeWorkloadEntryImage }
+  ];
+
+  if (serverConfig.ambientEnabled) {
+    nodeBadges.push({ label: t('Waypoint'), icon: badgeWaypointImage });
   }
-];
+
+  return [
+    {
+      title: t('Node Shapes'),
+      data: [
+        { label: t('Workload'), icon: workloadImage },
+        { label: t('App'), icon: appImage },
+        { label: t('Operation'), icon: operationImage },
+        { label: t('Service'), icon: serviceImage },
+        { label: t('Service Entry'), icon: serviceEntryImage }
+      ]
+    },
+    {
+      title: t('Node Colors'),
+      data: [
+        { label: t('Healthy'), icon: nodeColorHealthyImage },
+        { label: t('Warn'), icon: nodeColorWarningImage },
+        { label: t('Unhealthy'), icon: nodeColorDangerImage },
+        { label: t('Idle'), icon: nodeColorIdleImage }
+      ]
+    },
+    {
+      title: t('Node Background'),
+      data: [
+        { label: t('Unselected Namespace'), icon: externalNamespaceImage },
+        { label: t('Restricted / External'), icon: restrictedNamespaceImage }
+      ]
+    },
+    {
+      title: t('Edges'),
+      data: [
+        { label: t('Failure'), icon: edgeDangerImage },
+        { label: t('Degraded'), icon: edgeWarnImage },
+        { label: t('Healthy'), icon: edgeSuccessImage },
+        { label: t('TCP Connection'), icon: edgeTcpImage },
+        { label: t('Idle'), icon: edgeIdlemage },
+        { label: t('mTLS (badge)'), icon: edgeMtlsImage }
+      ]
+    },
+    {
+      title: t('Traffic Animation'),
+      data: [
+        { label: t('Healthy Request'), icon: trafficHealthyImage },
+        { label: t('Failed Request'), icon: trafficFailedImage },
+        { label: t('TCP Traffic'), icon: trafficTcpImage }
+      ]
+    },
+    {
+      title: t('Node Badges'),
+      isBadge: true,
+      data: nodeBadges
+    }
+  ];
+};
diff --git a/frontend/src/pages/GraphPF/GraphLegendPF.tsx b/frontend/src/pages/GraphPF/GraphLegendPF.tsx
index 7dded2123..fb2fe9552 100644
--- a/frontend/src/pages/GraphPF/GraphLegendPF.tsx
+++ b/frontend/src/pages/GraphPF/GraphLegendPF.tsx
@@ -99,7 +99,7 @@ export const GraphLegendPF: React.FC<GraphLegendPFProps> = (props: GraphLegendPF
         </Tooltip>
       </div>
 
-      <div className={bodyStyle}>{renderGraphLegendList(legendData)}</div>
+      <div className={bodyStyle}>{renderGraphLegendList(legendData())}</div>
     </div>
   );
 };
diff --git a/frontend/src/pages/GraphPF/GraphPFElems.tsx b/frontend/src/pages/GraphPF/GraphPFElems.tsx
index 70111628f..dd8f39333 100644
--- a/frontend/src/pages/GraphPF/GraphPFElems.tsx
+++ b/frontend/src/pages/GraphPF/GraphPFElems.tsx
@@ -115,7 +115,8 @@ const badgeMap = new Map<string, IconType>()
   .set('RT', icons.istio.requestTimeout) // clock
   .set('TS', icons.istio.trafficShifting) // share-alt
   .set('VS', icons.istio.virtualService) // code-branch
-  .set('WE', icons.istio.workloadEntry); // pf-icon-virtual-machine
+  .set('WE', icons.istio.workloadEntry) // pf-icon-virtual-machine
+  .set('WA', icons.istio.waypoint); // pf icon for infrastructure
 
 const EdgeColor = PFColors.Success;
 const EdgeColorDead = PFColors.Black500;
@@ -254,6 +255,11 @@ export const setNodeLabel = (node: NodeModel, nodeMap: NodeMap, settings: GraphP
     if (data.isGateway?.egressInfo?.hostnames?.length !== undefined) {
       data.labelIcon = <span className={`${badgeMap.get('GW')?.className}`}></span>;
     }
+    if (data.isWaypoint) {
+      data.labelIcon = (
+        <span className={`${badgeMap.get('WA')?.className}`} style={{ marginBottom: '1px', marginLeft: '2px' }}></span>
+      );
+    }
   }
 
   // Content portion of label (i.e. the text)...
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 7eeac2b9d..6598d5150 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1517,56 +1517,111 @@
   resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
   integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==
 
+"@esbuild/aix-ppc64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
+  integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
+
 "@esbuild/android-arm64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4"
   integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==
 
+"@esbuild/android-arm64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
+  integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
+
 "@esbuild/android-arm@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824"
   integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==
 
+"@esbuild/android-arm@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
+  integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
+
 "@esbuild/android-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d"
   integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==
 
+"@esbuild/android-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
+  integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
+
 "@esbuild/darwin-arm64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e"
   integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
 
+"@esbuild/darwin-arm64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
+  integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
+
 "@esbuild/darwin-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd"
   integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==
 
+"@esbuild/darwin-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
+  integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
+
 "@esbuild/freebsd-arm64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487"
   integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==
 
+"@esbuild/freebsd-arm64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
+  integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
+
 "@esbuild/freebsd-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c"
   integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==
 
+"@esbuild/freebsd-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
+  integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
+
 "@esbuild/linux-arm64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b"
   integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==
 
+"@esbuild/linux-arm64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
+  integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
+
 "@esbuild/linux-arm@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef"
   integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==
 
+"@esbuild/linux-arm@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
+  integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
+
 "@esbuild/linux-ia32@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601"
   integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==
 
+"@esbuild/linux-ia32@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
+  integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
+
 "@esbuild/linux-loong64@0.14.54":
   version "0.14.54"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
@@ -1577,61 +1632,121 @@
   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299"
   integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==
 
+"@esbuild/linux-loong64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
+  integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
+
 "@esbuild/linux-mips64el@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec"
   integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==
 
+"@esbuild/linux-mips64el@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
+  integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
+
 "@esbuild/linux-ppc64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8"
   integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==
 
+"@esbuild/linux-ppc64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
+  integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
+
 "@esbuild/linux-riscv64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf"
   integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==
 
+"@esbuild/linux-riscv64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
+  integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
+
 "@esbuild/linux-s390x@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8"
   integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==
 
+"@esbuild/linux-s390x@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
+  integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
+
 "@esbuild/linux-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78"
   integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
 
+"@esbuild/linux-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
+  integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
+
 "@esbuild/netbsd-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b"
   integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==
 
+"@esbuild/netbsd-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
+  integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
+
 "@esbuild/openbsd-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0"
   integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==
 
+"@esbuild/openbsd-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
+  integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
+
 "@esbuild/sunos-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30"
   integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==
 
+"@esbuild/sunos-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
+  integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
+
 "@esbuild/win32-arm64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae"
   integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==
 
+"@esbuild/win32-arm64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
+  integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
+
 "@esbuild/win32-ia32@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67"
   integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==
 
+"@esbuild/win32-ia32@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
+  integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
+
 "@esbuild/win32-x64@0.19.12":
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
   integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
 
+"@esbuild/win32-x64@0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
+  integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
+
 "@eslint-community/eslint-utils@^4.2.0":
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@@ -3997,13 +4112,13 @@ bluebird@^3.5.5, bluebird@^3.7.2:
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-body-parser@1.20.1:
-  version "1.20.1"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
-  integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
+body-parser@1.20.2:
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
+  integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
   dependencies:
     bytes "3.1.2"
-    content-type "~1.0.4"
+    content-type "~1.0.5"
     debug "2.6.9"
     depd "2.0.0"
     destroy "1.2.0"
@@ -4011,7 +4126,7 @@ body-parser@1.20.1:
     iconv-lite "0.4.24"
     on-finished "2.4.1"
     qs "6.11.0"
-    raw-body "2.5.1"
+    raw-body "2.5.2"
     type-is "~1.6.18"
     unpipe "1.0.0"
 
@@ -4050,12 +4165,12 @@ brace-expansion@^2.0.1:
   dependencies:
     balanced-match "^1.0.0"
 
-braces@^3.0.2, braces@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
-  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+braces@^3.0.3, braces@~3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+  integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
   dependencies:
-    fill-range "^7.0.1"
+    fill-range "^7.1.1"
 
 broccoli-node-api@^1.7.0:
   version "1.7.0"
@@ -4521,10 +4636,10 @@ commander@^8.3.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
 
-commander@~11.1.0:
-  version "11.1.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
-  integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==
+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==
 
 common-ancestor-path@^1.0.1:
   version "1.0.1"
@@ -4588,7 +4703,7 @@ content-disposition@0.5.4:
   dependencies:
     safe-buffer "5.2.1"
 
-content-type@~1.0.4:
+content-type@~1.0.4, content-type@~1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
   integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
@@ -4608,10 +4723,10 @@ cookie-signature@1.0.6:
   resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
   integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
 
-cookie@0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
-  integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+cookie@0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+  integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
 
 copy-to-clipboard@^3:
   version "3.3.1"
@@ -5326,11 +5441,6 @@ dayjs@^1.10.4:
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a"
   integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==
 
-de-indent@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
-  integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
-
 debug@2.6.9, debug@^2.2.0, debug@^2.6.0:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -5689,9 +5799,9 @@ ee-first@1.1.1:
   integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
 
 ejs@^3.1.6:
-  version "3.1.9"
-  resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
-  integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==
+  version "3.1.10"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
+  integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
   dependencies:
     jake "^10.8.5"
 
@@ -6038,7 +6148,7 @@ esbuild@^0.14.23:
     esbuild-windows-64 "0.14.54"
     esbuild-windows-arm64 "0.14.54"
 
-esbuild@^0.19.0, esbuild@^0.19.4:
+esbuild@^0.19.4:
   version "0.19.12"
   resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04"
   integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
@@ -6067,6 +6177,35 @@ esbuild@^0.19.0, esbuild@^0.19.4:
     "@esbuild/win32-ia32" "0.19.12"
     "@esbuild/win32-x64" "0.19.12"
 
+esbuild@^0.20.1:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1"
+  integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==
+  optionalDependencies:
+    "@esbuild/aix-ppc64" "0.20.2"
+    "@esbuild/android-arm" "0.20.2"
+    "@esbuild/android-arm64" "0.20.2"
+    "@esbuild/android-x64" "0.20.2"
+    "@esbuild/darwin-arm64" "0.20.2"
+    "@esbuild/darwin-x64" "0.20.2"
+    "@esbuild/freebsd-arm64" "0.20.2"
+    "@esbuild/freebsd-x64" "0.20.2"
+    "@esbuild/linux-arm" "0.20.2"
+    "@esbuild/linux-arm64" "0.20.2"
+    "@esbuild/linux-ia32" "0.20.2"
+    "@esbuild/linux-loong64" "0.20.2"
+    "@esbuild/linux-mips64el" "0.20.2"
+    "@esbuild/linux-ppc64" "0.20.2"
+    "@esbuild/linux-riscv64" "0.20.2"
+    "@esbuild/linux-s390x" "0.20.2"
+    "@esbuild/linux-x64" "0.20.2"
+    "@esbuild/netbsd-x64" "0.20.2"
+    "@esbuild/openbsd-x64" "0.20.2"
+    "@esbuild/sunos-x64" "0.20.2"
+    "@esbuild/win32-arm64" "0.20.2"
+    "@esbuild/win32-ia32" "0.20.2"
+    "@esbuild/win32-x64" "0.20.2"
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -6448,16 +6587,16 @@ expect@^27.5.1:
     jest-message-util "^27.5.1"
 
 express@^4.17.3:
-  version "4.18.2"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
-  integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
+  version "4.19.2"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
+  integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
   dependencies:
     accepts "~1.3.8"
     array-flatten "1.1.1"
-    body-parser "1.20.1"
+    body-parser "1.20.2"
     content-disposition "0.5.4"
     content-type "~1.0.4"
-    cookie "0.5.0"
+    cookie "0.6.0"
     cookie-signature "1.0.6"
     debug "2.6.9"
     depd "2.0.0"
@@ -6621,10 +6760,10 @@ filesize@^8.0.6:
   resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8"
   integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==
 
-fill-range@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
-  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+fill-range@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+  integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
   dependencies:
     to-regex-range "^5.0.1"
 
@@ -7447,17 +7586,18 @@ i18next-http-backend@^2.5.0:
   dependencies:
     cross-fetch "4.0.0"
 
-i18next-parser@^8.12.0:
-  version "8.12.0"
-  resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-8.12.0.tgz#1ed3c9acff82c17619ab6d5f232f8dc7f3fb572f"
-  integrity sha512-DUDFX3/nkECyd/zCqZoudTEtokMhOVX+zN8X8JasvYd/Lc/PsMcYuO2n0R7yzLiohXLj/7Hp20x0vAbuY8wo0Q==
+i18next-parser@^9.0.1:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-9.0.1.tgz#208917d1e6b524dcee81aaa6b71ef7572cbc1832"
+  integrity sha512-/Pr93/yEBdwsMKRsk4Zn63K368ALhzh8BRVrM6JNGOHy86ZKpiNJI6m8l1S/4T4Ofy1J4dlwkD7N98M70GP4aA==
   dependencies:
+    "@babel/runtime" "^7.23.2"
     broccoli-plugin "^4.0.7"
     cheerio "^1.0.0-rc.2"
     colors "1.4.0"
-    commander "~11.1.0"
+    commander "~12.1.0"
     eol "^0.9.1"
-    esbuild "^0.19.0"
+    esbuild "^0.20.1"
     fs-extra "^11.1.0"
     gulp-sort "^2.0.0"
     i18next "^23.5.1"
@@ -7468,7 +7608,6 @@ i18next-parser@^8.12.0:
     typescript "^5.0.4"
     vinyl "~3.0.0"
     vinyl-fs "^4.0.0"
-    vue-template-compiler "^2.6.11"
 
 i18next@^23.10.1:
   version "23.10.1"
@@ -8967,12 +9106,12 @@ micro-memoize@4.0.9:
   resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.0.9.tgz#b44a38c9dffbee1cefc2fd139bc8947952268b62"
   integrity sha512-Z2uZi/IUMGQDCXASdujXRqrXXEwSY0XffUrAOllhqzQI3wpUyZbiZTiE2JuYC0HSG2G7DbCS5jZmsEKEGZuemg==
 
-micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
-  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"
+  integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
   dependencies:
-    braces "^3.0.2"
+    braces "^3.0.3"
     picomatch "^2.3.1"
 
 mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
@@ -10465,10 +10604,10 @@ range-parser@^1.2.1, range-parser@~1.2.1:
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
   integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
 
-raw-body@2.5.1:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
-  integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+raw-body@2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+  integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
   dependencies:
     bytes "3.1.2"
     http-errors "2.0.0"
@@ -11837,7 +11976,14 @@ stringify-object@^3.3.0:
     is-obj "^1.0.1"
     is-regexp "^1.0.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -12920,14 +13066,6 @@ void-elements@3.1.0:
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
   integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
 
-vue-template-compiler@^2.6.11:
-  version "2.7.15"
-  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.15.tgz#ec88ba8ceafe0f17a528b89c57e01e02da92b0de"
-  integrity sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og==
-  dependencies:
-    de-indent "^1.0.2"
-    he "^1.2.0"
-
 w3c-hr-time@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
@@ -13012,9 +13150,9 @@ webidl-conversions@^6.1.0:
   integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
 
 webpack-dev-middleware@^5.3.1:
-  version "5.3.3"
-  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f"
-  integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==
+  version "5.3.4"
+  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517"
+  integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==
   dependencies:
     colorette "^2.0.10"
     memfs "^3.4.3"
@@ -13384,8 +13522,7 @@ workerpool@6.2.1:
   resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
   integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
-  name wrap-ansi-cjs
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -13403,6 +13540,15 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -13428,14 +13574,14 @@ write-file-atomic@^3.0.0:
     typedarray-to-buffer "^3.1.5"
 
 ws@^7.4.6:
-  version "7.5.9"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
-  integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
+  version "7.5.10"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
+  integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
 
 ws@^8.13.0:
-  version "8.13.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
-  integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
+  version "8.18.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
+  integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
 
 xml-name-validator@^3.0.0:
   version "3.0.0"
diff --git a/go.mod b/go.mod
index 3bb83c63a..cd62c307a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/kiali/kiali
 
-go 1.22.4
+go 1.22.5
 
 require (
 	github.com/NYTimes/gziphandler v1.1.1
diff --git a/kubernetes/cache/cache.go b/kubernetes/cache/cache.go
index fa319c2c1..fa4a2664d 100644
--- a/kubernetes/cache/cache.go
+++ b/kubernetes/cache/cache.go
@@ -26,8 +26,10 @@ const (
 	waypointExpirationTime     = 1 * time.Minute
 )
 
-const kialiCacheMeshKey = "mesh"
-const ztunnelApp = "ztunnel"
+const (
+	kialiCacheMeshKey = "mesh"
+	ztunnelApp        = "ztunnel"
+)
 
 // KialiCache stores both kube objects and non-kube related data such as pods' proxy status.
 // It is exclusively used by the business layer where it's expected to be a singleton.
@@ -307,6 +309,10 @@ type namespacesKey struct {
 	token   string
 }
 
+func (n namespacesKey) String() string {
+	return fmt.Sprintf("cluster: %s\ttoken: xxx", n.cluster)
+}
+
 func (c *kialiCacheImpl) GetNamespace(cluster string, token string, namespace string) (models.Namespace, bool) {
 	c.namespacesLock.RLock()
 	defer c.namespacesLock.RUnlock()
diff --git a/kubernetes/client_factory.go b/kubernetes/client_factory.go
index 40144b9f1..2ce684300 100644
--- a/kubernetes/client_factory.go
+++ b/kubernetes/client_factory.go
@@ -49,6 +49,8 @@ type clientFactory struct {
 	// "in cluster" config. This name comes from the istio cluster id.
 	homeCluster string
 
+	kialiConfig *kialiConfig.Config
+
 	// mutex for when accessing the stored clients
 	mutex sync.RWMutex
 
@@ -92,7 +94,7 @@ func getClientFactory(conf kialiConfig.Config) (*clientFactory, error) {
 		Burst:           conf.KubernetesConfig.Burst,
 	}
 
-	return newClientFactory(&baseConfig)
+	return newClientFactory(&conf, &baseConfig)
 }
 
 // NewClientFactory creates a new client factory that can be transitory.
@@ -118,8 +120,9 @@ func NewClientFactory(ctx context.Context, conf kialiConfig.Config) (ClientFacto
 
 // newClientFactory allows for specifying the config and expiry duration
 // Mock friendly for testing purposes
-func newClientFactory(restConfig *rest.Config) (*clientFactory, error) {
+func newClientFactory(conf *kialiConfig.Config, restConfig *rest.Config) (*clientFactory, error) {
 	f := &clientFactory{
+		kialiConfig:     conf,
 		baseRestConfig:  restConfig,
 		clientEntries:   make(map[string]map[string]ClientInterface),
 		recycleChan:     make(chan string),
@@ -179,40 +182,22 @@ func (cf *clientFactory) newClient(authInfo *api.AuthInfo, expirationTime time.D
 	// So, if OpenID strategy is active, check if a proxy is configured.
 	// If there is, use it UNLESS the token is the one of the Kiali SA. If
 	// the token is the one of the Kiali SA, the proxy can be bypassed.
-	cfg := kialiConfig.Get()
-	if cfg.Auth.Strategy == kialiConfig.AuthStrategyOpenId && cfg.Auth.OpenId.ApiProxy != "" && cfg.Auth.OpenId.ApiProxyCAData != "" {
-
-		var kialiToken string
-		var err error
-
-		if cluster == cf.homeCluster {
-			kialiToken, _, err = GetKialiTokenForHomeCluster()
-		} else {
-			kialiToken, err = cf.GetSAClient(cluster).GetToken(), nil
-		}
-
+	if cf.kialiConfig.Auth.Strategy == kialiConfig.AuthStrategyOpenId && cf.kialiConfig.Auth.OpenId.ApiProxy != "" && cf.kialiConfig.Auth.OpenId.ApiProxyCAData != "" {
+		// Override the CA data on the client with the proxy CA from the Kiali config.
+		caData := cf.kialiConfig.Auth.OpenId.ApiProxyCAData
+		rootCaDecoded, err := base64.StdEncoding.DecodeString(caData)
 		if err != nil {
 			return nil, err
 		}
 
-		// User token and not Kiali SA token so use the proxy.
-		if kialiToken != authInfo.Token {
-			// Override the CA data on the client with the proxy CA from the Kiali config.
-			caData := cfg.Auth.OpenId.ApiProxyCAData
-			rootCaDecoded, err := base64.StdEncoding.DecodeString(caData)
-			if err != nil {
-				return nil, err
-			}
-
-			config.TLSClientConfig = rest.TLSClientConfig{
-				CAData: []byte(rootCaDecoded),
-			}
-			config.Host = cfg.Auth.OpenId.ApiProxy
+		config.TLSClientConfig = rest.TLSClientConfig{
+			CAData: []byte(rootCaDecoded),
 		}
+		config.Host = cf.kialiConfig.Auth.OpenId.ApiProxy
 	}
 
 	// Impersonation is valid only for header authentication strategy
-	if cfg.Auth.Strategy == kialiConfig.AuthStrategyHeader && authInfo.Impersonate != "" {
+	if cf.kialiConfig.Auth.Strategy == kialiConfig.AuthStrategyHeader && authInfo.Impersonate != "" {
 		config.Impersonate.UserName = authInfo.Impersonate
 		config.Impersonate.Groups = authInfo.ImpersonateGroups
 		config.Impersonate.Extra = authInfo.ImpersonateUserExtra
@@ -246,13 +231,8 @@ func (cf *clientFactory) newClient(authInfo *api.AuthInfo, expirationTime time.D
 		}
 
 		// Replace the Kiali SA token with the user's auth token.
-		// Only if we are not in an anonymous mode
-		// and if we don't use OpenID with RBAC is disable.
-		if !(cfg.Auth.Strategy == kialiConfig.AuthStrategyAnonymous) &&
-			!(cfg.Auth.Strategy == kialiConfig.AuthStrategyOpenId && cfg.Auth.OpenId.DisableRBAC) {
-			remoteConfig.BearerToken = authInfo.Token
-			remoteConfig.BearerTokenFile = ""
-		}
+		remoteConfig.BearerToken = authInfo.Token
+		remoteConfig.BearerTokenFile = ""
 
 		newClient, err = newClientWithRemoteClusterInfo(remoteConfig, &clusterInfo)
 		if err != nil {
@@ -296,11 +276,19 @@ func (cf *clientFactory) GetSAClients() map[string]ClientInterface {
 
 // getClient returns a client for the specified token. Creating one if necessary.
 func (cf *clientFactory) GetClient(authInfo *api.AuthInfo, cluster string) (ClientInterface, error) {
+	if cf.kialiConfig.IsRBACDisabled() {
+		return cf.GetSAClient(cluster), nil
+	}
+
 	return cf.getRecycleClient(authInfo, defaultExpirationTime, cluster)
 }
 
 // getClient returns a client for the specified token. Creating one if necessary.
 func (cf *clientFactory) GetClients(authInfos map[string]*api.AuthInfo) (map[string]ClientInterface, error) {
+	if cf.kialiConfig.IsRBACDisabled() {
+		return cf.GetSAClients(), nil
+	}
+
 	clients := make(map[string]ClientInterface)
 	for cluster, authInfo := range authInfos {
 		ci, err := cf.getRecycleClient(authInfo, defaultExpirationTime, cluster)
diff --git a/store/expiration_store.go b/store/expiration_store.go
index fdd80ba96..0f00e98d4 100644
--- a/store/expiration_store.go
+++ b/store/expiration_store.go
@@ -90,7 +90,7 @@ func (s *ExpirationStore[K, V]) checkAndRemoveExpiredKeys(ctx context.Context) <
 					}
 
 					if time.Now().After(expiration) {
-						log.Tracef("Key %v expired. Removing from store", key)
+						log.Tracef("Key '%v' expired. Removing from store", key)
 						s.Remove(key)
 					}
 				}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 16e0dc23d..85fd4cfd7 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/kiali/kiali/util"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestSecretFileOverrides(t *testing.T) {
@@ -366,3 +367,63 @@ func TestValidateAuthStrategy(t *testing.T) {
 		}
 	}
 }
+
+func TestIsRBACDisabled(t *testing.T) {
+	cases := map[string]struct {
+		authConfig         AuthConfig
+		expectRBACDisabled bool
+	}{
+		"anonymous should have RBAC disabled": {
+			authConfig: AuthConfig{
+				Strategy: AuthStrategyAnonymous,
+			},
+			expectRBACDisabled: true,
+		},
+		"openid with rbac disabled should have RBAC disabled": {
+			authConfig: AuthConfig{
+				Strategy: AuthStrategyOpenId,
+				OpenId: OpenIdConfig{
+					DisableRBAC: true,
+				},
+			},
+			expectRBACDisabled: true,
+		},
+		"openid with rbac enabled should have RBAC enabled": {
+			authConfig: AuthConfig{
+				Strategy: AuthStrategyOpenId,
+				OpenId: OpenIdConfig{
+					DisableRBAC: false,
+				},
+			},
+			expectRBACDisabled: false,
+		},
+		"openshift should have RBAC enabled": {
+			authConfig: AuthConfig{
+				Strategy: AuthStrategyOpenshift,
+			},
+			expectRBACDisabled: false,
+		},
+		"token should have RBAC enabled": {
+			authConfig: AuthConfig{
+				Strategy: AuthStrategyToken,
+			},
+			expectRBACDisabled: false,
+		},
+		"header should have RBAC enabled": {
+			authConfig: AuthConfig{
+				Strategy: AuthStrategyHeader,
+			},
+			expectRBACDisabled: false,
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			require := require.New(t)
+
+			conf := NewConfig()
+			conf.Auth = tc.authConfig
+
+			require.Equal(tc.expectRBACDisabled, conf.IsRBACDisabled())
+		})
+	}
+}
diff --git a/kubernetes/client_factory_test.go b/kubernetes/client_factory_test.go
index 3692bf18e..b6ba74974 100644
--- a/kubernetes/client_factory_test.go
+++ b/kubernetes/client_factory_test.go
@@ -114,6 +114,7 @@ func TestConcurrentClientExpiration(t *testing.T) {
 // TestConcurrentClientFactory test Concurrently create ClientFactory
 func TestConcurrentClientFactory(t *testing.T) {
 	require := require.New(t)
+	conf := config.NewConfig()
 	istioConfig := rest.Config{}
 	count := 100
 
@@ -125,7 +126,7 @@ func TestConcurrentClientFactory(t *testing.T) {
 	for i := 0; i < count; i++ {
 		go func() {
 			defer wg.Done()
-			_, err := newClientFactory(&istioConfig)
+			_, err := newClientFactory(conf, &istioConfig)
 			require.NoError(err)
 		}()
 	}
@@ -387,39 +388,73 @@ func setGlobalKialiSAToken(t *testing.T) {
 }
 
 func TestClientCreatedWithProxyInfo(t *testing.T) {
-	require := require.New(t)
-
-	cfg := config.NewConfig()
-	cfg.Deployment.RemoteSecretPath = t.TempDir() // Random dir so that the remote secret isn't read if it exists.
-	cfg.Auth.Strategy = config.AuthStrategyOpenId
-	cfg.Auth.OpenId.ApiProxyCAData = base64.StdEncoding.EncodeToString(proxyCAData)
-	cfg.Auth.OpenId.ApiProxy = "https://api-proxy:8443"
-	SetConfig(t, *cfg)
-
-	clientFactory := NewTestingClientFactory(t)
+	cases := map[string]struct {
+		auth              config.AuthConfig
+		expectProxyConfig bool
+	}{
+		"openid with proxy defined should have proxy on client": {
+			auth: config.AuthConfig{
+				Strategy: config.AuthStrategyOpenId,
+				OpenId: config.OpenIdConfig{
+					ApiProxy:       "https://api-proxy:8443",
+					ApiProxyCAData: base64.StdEncoding.EncodeToString(proxyCAData),
+				},
+			},
+			expectProxyConfig: true,
+		},
+		"openid with rbac disabled and proxy defined should not have proxy on client": {
+			auth: config.AuthConfig{
+				Strategy: config.AuthStrategyOpenId,
+				OpenId: config.OpenIdConfig{
+					DisableRBAC:    true,
+					ApiProxy:       "https://api-proxy:8443",
+					ApiProxyCAData: base64.StdEncoding.EncodeToString(proxyCAData),
+				},
+			},
+			expectProxyConfig: false,
+		},
+		"anonymous with proxy defined should not have proxy on client": {
+			auth: config.AuthConfig{
+				Strategy: config.AuthStrategyAnonymous,
+				OpenId: config.OpenIdConfig{
+					DisableRBAC:    true,
+					ApiProxy:       "https://api-proxy:8443",
+					ApiProxyCAData: base64.StdEncoding.EncodeToString(proxyCAData),
+				},
+			},
+			expectProxyConfig: false,
+		},
+	}
 
-	// Regular clients should have the proxy info
-	client, err := clientFactory.GetClient(api.NewAuthInfo(), cfg.KubernetesConfig.ClusterName)
-	require.NoError(err)
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			require := require.New(t)
 
-	require.Equal(cfg.Auth.OpenId.ApiProxy, client.ClusterInfo().ClientConfig.Host)
-	require.Equal(proxyCAData, client.ClusterInfo().ClientConfig.CAData)
+			cfg := config.NewConfig()
+			cfg.Deployment.RemoteSecretPath = t.TempDir() // Random dir so that the remote secret isn't read if it exists.
+			cfg.Auth = tc.auth
+			SetConfig(t, *cfg)
 
-	// Service account clients should not have the proxy info.
-	// Two ways to get a SA client: 1. GetClient with SA token and 2. GetSAClient
-	setGlobalKialiSAToken(t)
-	authInfo := api.NewAuthInfo()
-	authInfo.Token = KialiTokenForHomeCluster
+			clientFactory := NewTestingClientFactory(t)
 
-	client, err = clientFactory.GetClient(authInfo, cfg.KubernetesConfig.ClusterName)
-	require.NoError(err)
+			// Regular clients should have the proxy info
+			client, err := clientFactory.GetClient(api.NewAuthInfo(), cfg.KubernetesConfig.ClusterName)
+			require.NoError(err)
 
-	require.NotEqual(cfg.Auth.OpenId.ApiProxy, client.ClusterInfo().ClientConfig.Host)
-	require.NotEqual(proxyCAData, client.ClusterInfo().ClientConfig.CAData)
+			if tc.expectProxyConfig {
+				require.Equal(cfg.Auth.OpenId.ApiProxy, client.ClusterInfo().ClientConfig.Host)
+				require.Equal(proxyCAData, client.ClusterInfo().ClientConfig.CAData)
+			} else {
+				require.NotEqual(cfg.Auth.OpenId.ApiProxy, client.ClusterInfo().ClientConfig.Host)
+				require.NotEqual(proxyCAData, client.ClusterInfo().ClientConfig.CAData)
+			}
 
-	client = clientFactory.GetSAClient(cfg.KubernetesConfig.ClusterName)
-	require.NotEqual(cfg.Auth.OpenId.ApiProxy, client.ClusterInfo().ClientConfig.Host)
-	require.NotEqual(proxyCAData, client.ClusterInfo().ClientConfig.CAData)
+			// Service account clients should not have the proxy info.
+			client = clientFactory.GetSAClient(cfg.KubernetesConfig.ClusterName)
+			require.NotEqual(cfg.Auth.OpenId.ApiProxy, client.ClusterInfo().ClientConfig.Host)
+			require.NotEqual(proxyCAData, client.ClusterInfo().ClientConfig.CAData)
+		})
+	}
 }
 
 func TestNewClientFactoryClosesRecycleWhenCTXCancelled(t *testing.T) {
@@ -506,3 +541,69 @@ func TestClientFactoryReturnsSAClientWhenConfigClusterNameIsEmpty(t *testing.T)
 
 	require.NotNil(clientFactory.GetSAHomeClusterClient())
 }
+
+func TestClientFactoryGetClients(t *testing.T) {
+	cases := map[string]struct {
+		auth               config.AuthConfig
+		authInfo           map[string]*api.AuthInfo
+		expectClientsEqual bool
+	}{
+		"anonymous strategy returns Service Account clients": {
+			auth: config.AuthConfig{
+				Strategy: config.AuthStrategyAnonymous,
+			},
+			authInfo:           map[string]*api.AuthInfo{"cluster1": api.NewAuthInfo()},
+			expectClientsEqual: true,
+		},
+		"anonymous strategy returns Service Account clients with nil auth info": {
+			auth: config.AuthConfig{
+				Strategy: config.AuthStrategyAnonymous,
+			},
+			authInfo:           nil,
+			expectClientsEqual: true,
+		},
+		"openid with rbac disabled returns Service Account clients": {
+			auth: config.AuthConfig{
+				Strategy: config.AuthStrategyOpenId,
+				OpenId: config.OpenIdConfig{
+					DisableRBAC: true,
+				},
+			},
+			authInfo:           map[string]*api.AuthInfo{"cluster1": api.NewAuthInfo()},
+			expectClientsEqual: true,
+		},
+		"openid with rbac enabled returns user clients": {
+			auth: config.AuthConfig{
+				Strategy: config.AuthStrategyOpenId,
+				OpenId: config.OpenIdConfig{
+					DisableRBAC: false,
+				},
+			},
+			authInfo:           map[string]*api.AuthInfo{"cluster1": api.NewAuthInfo()},
+			expectClientsEqual: false,
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			require := require.New(t)
+			conf := config.NewConfig()
+			conf.Auth = tc.auth
+			conf.KubernetesConfig.ClusterName = "cluster1"
+			SetConfig(t, *conf)
+
+			clientFactory := NewTestingClientFactory(t)
+			clients, err := clientFactory.GetClients(tc.authInfo)
+			require.NoError(err)
+			require.Len(clients, 1)
+
+			saClients := clientFactory.GetSAClients()
+			require.Len(saClients, 1)
+
+			if tc.expectClientsEqual {
+				require.Equal(clients["cluster1"], saClients["cluster1"])
+			} else {
+				require.NotEqual(clients["cluster1"], saClients["cluster1"])
+			}
+		})
+	}
+}
diff --git a/kubernetes/testing.go b/kubernetes/testing.go
index c86456d6c..1bd58dfa6 100644
--- a/kubernetes/testing.go
+++ b/kubernetes/testing.go
@@ -56,7 +56,7 @@ func NewTestingClientFactory(t *testing.T) *clientFactory {
 	}
 
 	clientConfig := rest.Config{}
-	client, err := newClientFactory(&clientConfig)
+	client, err := newClientFactory(config.Get(), &clientConfig)
 	if err != nil {
 		t.Fatalf("Error creating client factory: %v", err)
 	}