Repository: kiali/kiali

Issue:
Multicluster
Kiali should support a mesh that spans multiple clusters providing a single unified view of the mesh.

There have been some past discussions around this: https://github.com/kiali/kiali/discussions/categories/multicluster. Initial "Alpha" support for multi cluster deployments will focus on Primary - Remote deployments where Kiali can read from multiple clusters but only write to the "home" cluster.

Alpha
- [x] https://github.com/kiali/kiali/issues/5919
- [x] https://github.com/kiali/kiali/issues/5920
- [x] https://github.com/kiali/kiali/issues/5921
- [x] https://github.com/kiali/kiali/issues/5922
- [x] https://github.com/kiali/kiali/issues/6028
- [x] #6026 
- [x] #6001
- [x] #6033
- [x] #5736 
- [x] #6007 
- [x] https://github.com/kiali/kiali/issues/5738
- [x] https://github.com/kiali/kiali/issues/5925
- [x] https://github.com/kiali/kiali/issues/5939
- [x] #5633
- [x] #5724
- [x] #5702
- [x] https://github.com/kiali/kiali/issues/5620
- [x] https://github.com/kiali/kiali/issues/5621
- [x] #5701
- [x] https://github.com/kiali/kiali/issues/6051
- [x] https://github.com/kiali/kiali/issues/6052
- [x] https://github.com/kiali/kiali/issues/6053
- [x] https://github.com/kiali/kiali/issues/6064
- [x] https://github.com/kiali/kiali/issues/6065
- [x] https://github.com/kiali/kiali/issues/6102
- [x] https://github.com/kiali/kiali/issues/6127
- [x] https://github.com/kiali/kiali/issues/6129
- [x] https://github.com/kiali/kiali/issues/6062
- [x] https://github.com/kiali/kiali/issues/6123
- [x] https://github.com/kiali/kiali/issues/6150
- [x] https://github.com/kiali/kiali/issues/6171
- [x] https://github.com/kiali/kiali/issues/6169
- [x] https://github.com/kiali/kiali/issues/6179
- [x] https://github.com/kiali/kiali/issues/6137
- [x] https://github.com/kiali/kiali/issues/6198
- [x] https://github.com/kiali/kiali/issues/5928
- [x] https://github.com/kiali/kiali/issues/6185
- [x] https://github.com/kiali/kiali/issues/6182
- [x] https://github.com/kiali/kiali/issues/6233
- [x] #6306 
- [x] https://github.com/kiali/kiali/issues/6333 

Beta
- [x] #6036
- [x] #6361
- [x] https://github.com/kiali/kiali/issues/6243
- [x] #6035
- [x] https://github.com/kiali/kiali/issues/6056
- [x] https://github.com/kiali/kiali/issues/6239
- [x] https://github.com/kiali/kiali/issues/6467
- [x] #6615 
- [x] #6626 
- [x] https://github.com/kiali/kiali/issues/6682
- [x] https://github.com/kiali/kiali/issues/6712
- [x] https://github.com/kiali/kiali/issues/6710
- [x] https://github.com/kiali/kiali/issues/6437
- [x] https://github.com/kiali/kiali/issues/5619
- [x] https://github.com/kiali/kiali/issues/5923

Stable
- [x] https://github.com/kiali/kiali/issues/5632
- [x] #5941
- [x] https://github.com/kiali/kiali/issues/6363

Future Enhancements
- [x] #7042 
- [x] #6034
- [x] #6360
- [ ] #5932
- [x] #6037

REF #5626 

Hints from issue comments:
@israel-hdez: Removing my assignment...@Aur0nd: I'm really hoping to see this finished/merged, the whole point having such a view is to be able to see exactly where the traffic flows, right? :) @jmazzitelli: > I'm really hoping to see this finished/merged, the whole point having such a view is to be able to see exactly where the traffic flows, right? :)

The Kiali team and community have been spending months on this epic with some of these features already in existing Kiali releases, and more to come.

But we figured... nah, forget it, and we abandoned it. ;)@nrfox: @Aur0nd you can already use Kiali with multi-cluster istio deployments. See https://kiali.io/docs/configuration/multi-cluster/ for more info. What's left in this issue as of today is mostly cleanup or enabling specific deployments (openshift).@jshaughn: @Aur0nd And thanks for your interest, please try Kiali multi-cluster support and give us feedback either via github or istio slack's #kiali channel!@nrfox: Closing this as completed since support for all deployment models has been added which was the goal of this epic. Future multi-cluster related work and enhancements can be tracked separately.

Solution patch:
diff --git a/.github/workflows/integration-tests-backend.yml b/.github/workflows/integration-tests-backend.yml
index 2f7a243d8..291bac758 100644
--- a/.github/workflows/integration-tests-backend.yml
+++ b/.github/workflows/integration-tests-backend.yml
@@ -68,6 +68,6 @@ jobs:
       - name: Get debug info when integration tests fail
         if: failure()
         run: |
-          kubectl logs -l app.kubernetes.io/name=kiali -n istio-system
+          kubectl logs -l app.kubernetes.io/name=kiali --tail=-1 --all-containers -n istio-system
           kubectl describe nodes
           kubectl get pods -l app.kubernetes.io/name=kiali -n istio-system -o yaml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 71a2447d0..ae71e6774 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,8 +16,9 @@ We gratefully welcome improvements to documentation as well as to code.
 
 Before you make a change, please:
 
-* Open an discussion or an issue describing the motivation of work (especially for large changes).
-* Let other maintainers to comment/review the proposal or validate if the work is not being addressed by other contributor.
+* Open a [discussion](https://github.com/kiali/kiali/discussions) or an [issue](https://github.com/kiali/kiali/issues) describing in detail the motivation of work. Regardless of the repo where the work should be done (server, UI, operator, or helm charts), all discussions and issues should be submitted to the main kiali/kiali repo using those links provided.
+* Let the maintainers comment on the question or refine the issue.
+* Before starting work, make sure maintainers have agreed that the work should be done and has added the issue to the backlog.
 * When the design/approach/discussion is ready, prepare a Pull Request with the changes.
 
 ### Good first issues
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
index 856212ae1..7900c9142 100644
--- a/GOVERNANCE.md
+++ b/GOVERNANCE.md
@@ -19,6 +19,7 @@ Current list of maintainers (alphabetically ordered):
 * [hhovsepy](https://github.com/hhovsepy)
 * [israel-hdez](https://github.com/israel-hdez)
 * [jmazzitelli](https://github.com/jmazzitelli)
+* [josunect](https://github.com/josunect)
 * [leandroberetta](https://github.com/leandroberetta)
 * [nrfox](https://github.com/nrfox)
 * [xunzhuo](https://github.com/xunzhuo)
@@ -66,11 +67,11 @@ both roles aren't mutually exclusive.
 Current list of testers (alphabetically ordered):
 
 * [FilipB](https://github.com/FilipB)
+* [matejnesuta](https://github.com/matejnesuta)
 * [mattmahoneyrh](https://github.com/mattmahoneyrh)
 * [pbajjuri20](https://github.com/pbajjuri20)
 * [prachiyadav](https://github.com/prachiyadav)
 * [ScriptingShrimp](https://github.com/ScriptingShrimp)
-* [skondkar](https://github.com/skondkar)
 
 ### Becoming a Tester
 
@@ -151,6 +152,7 @@ Contact the Maintainers about changing to Emeritus status, or reducing your cont
 * [gbaufake](https://github.com/gbaufake)
 * [josejulio](https://github.com/josejulio)
 * [jotak](https://github.com/jotak)
+* [jstickler](https://github.com/JStickler)
 * [jpkrohling](https://github.com/jpkrohling)
 * [kevinearls](https://github.com/kevinearls)
 * [lucasponce](https://github.com/lucasponce)
@@ -160,6 +162,7 @@ Contact the Maintainers about changing to Emeritus status, or reducing your cont
 * [mwringe](https://github.com/mwringe)
 * [pavolloffay](https://github.com/pavolloffay)
 * [serenamarie125](https://github.com/serenamarie125)
+* [skondkar](https://github.com/skondkar)
 * [xeviknal](https://github.com/xeviknal)
 
 ## Voting
diff --git a/Makefile b/Makefile
index a47bcbbc2..46b847392 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ TARGET_ARCHS ?= amd64 arm64 s390x ppc64le
 
 # Identifies the current build.
 # These will be embedded in the app and displayed when it starts.
-VERSION ?= v1.61.0-SNAPSHOT
+VERSION ?= v1.64.0-SNAPSHOT
 COMMIT_HASH ?= $(shell git rev-parse HEAD)
 
 # The path where the UI project has been git cloned. The UI should
@@ -29,7 +29,7 @@ VERSION_LABEL ?= ${VERSION}
 # The go commands and the minimum Go version that must be used to build the app.
 GO ?= go
 GOFMT ?= $(shell ${GO} env GOROOT)/bin/gofmt
-GO_VERSION_KIALI = 1.18.7
+GO_VERSION_KIALI = 1.19.4
 GO_TEST_FLAGS ?=
 
 # Identifies the Kiali container image that will be built.
diff --git a/business/authentication/openid_auth_controller.go b/business/authentication/openid_auth_controller.go
index 7cae6a769..f32ba9a3b 100644
--- a/business/authentication/openid_auth_controller.go
+++ b/business/authentication/openid_auth_controller.go
@@ -87,15 +87,12 @@ type oidcSessionPayload struct {
 	Token string `json:"token,omitempty"`
 }
 
-// badOidcRequest is an helper type implementing Go's error interface. It's used to assist in
-// error handling on the OpenId authentication flow. For the authorization code flow, this
-// indicates that the authentication is not going to be handled and the http request should be
-// handled by the next handler. In the implicit flow, the error details are returned to the user.
-//
-// The difference in behavior is because on the implicit flow the auth request is done to a dedicated
-// authentication endpoint, so it is evident that any error should be returned to the user. In contrast,
-// on the authorization code flow, the authentication is done to the Kiali's web_root, so it is hard to
-// differentiate between an auth callback versus a first user request to Kiali.
+// badOidcRequest is a helper type implementing Go's error interface. It's used to assist in
+// error handling on the OpenId authentication flow. Since authentication is initiated via
+// Kiali's web_root, it is hard to differentiate between an auth callback versus a first user
+// request to Kiali. So, if this error is raised, it indicates that the authentication
+// is not going to be handled and the http request should be passed to the next handler in
+// the chain of the web_root endpoint.
 type badOidcRequest struct {
 	// Detail contains the description of the error.
 	Detail string
@@ -107,8 +104,8 @@ func (e badOidcRequest) Error() string {
 }
 
 // OpenIdAuthController contains the backing logic to implement
-// Kiali's "openid" authentication strategy. The implicit flow and
-// the authorization code flow are implemented.
+// Kiali's "openid" authentication strategy. Only
+// the authorization code flow is implemented.
 //
 // RBAC is supported, although it requires that the cluster is configured
 // with OpenId integration. Thus, it is possible to turn off RBAC
@@ -137,41 +134,15 @@ func NewOpenIdAuthController(persistor SessionPersistor, businessInstantiator fu
 	}
 }
 
-// Authenticate is the entry point to handle OpenId authentication using the implicit flow. The HTTP request
-// should contain "id_token" and "state" as URL parameters. If RBAC is enabled, the id_token should be
-// valid to be used in the Kubernetes API (thus, privileges are verified to allow login); else, only token
-// validity is checked and users will share the same privileges.
-// An AuthenticationFailureError is returned if the authentication failed. Any
-// other kind of error means that something unexpected happened.
+// Authenticate was the entry point to handle OpenId authentication using the implicit flow. Support
+// for the implicit flow has been removed. This is left here, because the "Authenticate" function is required
+// by the AuthController interface which must be implemented by all auth controllers. So, this simply
+// returns an error.
 func (c OpenIdAuthController) Authenticate(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) {
-	flow := openidFlowHelper{businessInstantiator: c.businessInstantiator}
-	sPayload := flow.
-		extractOpenIdCallbackParams(r).
-		callbackCleanup(w).
-		checkOpenIdImplicitFlowParams().
-		validateOpenIdState().
-		parseOpenIdToken().
-		validateOpenIdNonceCode().
-		checkAllowedDomains().
-		checkUserPrivileges().
-		createSession(r, w, c.SessionStore)
-	err := flow.Error
-
-	if err != nil {
-		if flow.ShouldTerminateSession {
-			c.SessionStore.TerminateSession(r, w)
-		}
-		return nil, err
-	}
-
-	return &UserSessionData{
-		ExpiresOn: flow.ExpiresOn,
-		Username:  sPayload.Subject,
-		AuthInfo:  &api.AuthInfo{Token: sPayload.Token},
-	}, nil
+	return nil, fmt.Errorf("support for OpenID's implicit flow has been removed")
 }
 
-// GetAuthCallbackHandler returns an http handler for authentication requests done to Kiali's web_root.
+// GetAuthCallbackHandler returns a http handler for authentication requests done to Kiali's web_root.
 // This handler catches callbacks from the OpenId server. If it cannot be determined that the request
 // is a callback from the authentication server, the request is passed to the fallbackHandler.
 func (c OpenIdAuthController) GetAuthCallbackHandler(fallbackHandler http.Handler) http.Handler {
@@ -274,7 +245,7 @@ func (c OpenIdAuthController) ValidateSession(r *http.Request, w http.ResponseWr
 		token = sPayload.Token
 	} else {
 		// If RBAC is off, it's assumed that the kubernetes cluster will reject the OpenId token.
-		// Instead, we use the Kiali token an this has the side effect that all users will share the
+		// Instead, we use the Kiali token and this has the side effect that all users will share the
 		// same privileges.
 		token, err = kubernetes.GetKialiToken()
 		if err != nil {
@@ -329,12 +300,13 @@ func (c OpenIdAuthController) authenticateWithAuthorizationCodeFlow(r *http.Requ
 
 	if flow.Error != nil {
 		if err, ok := flow.Error.(*badOidcRequest); ok {
-			log.Errorf("Not handling OpenId code flow authentication: %s", err.Detail)
+			log.Debugf("Not handling OpenId code flow authentication: %s", err.Detail)
 			fallbackHandler.ServeHTTP(w, r)
 		} else {
 			if flow.ShouldTerminateSession {
 				c.SessionStore.TerminateSession(r, w)
 			}
+			log.Warningf("Authentication rejected: %s", flow.Error.Error())
 			http.Redirect(w, r, fmt.Sprintf("%s?openid_error=%s", webRootWithSlash, url.QueryEscape(flow.Error.Error())), http.StatusFound)
 		}
 		return
@@ -345,7 +317,7 @@ func (c OpenIdAuthController) authenticateWithAuthorizationCodeFlow(r *http.Requ
 }
 
 // redirectToAuthServerHandler prepares the redirection to initiate authentication with an OpenId Server.
-// It find what's the authentication endpoint of the OpenId server to redirect the user to. Then, creates
+// It finds what's the authentication endpoint of the OpenId server to redirect the user to. Then, creates
 // the "nonce" and the "state" codes and forms the final URL to reply with a "302 Found" HTTP status and
 // post the redirection in a "Location" HTTP header, with the needed parameters given the OpenId server
 // capabilities. A Cookie is set to store the source of the calculated codes and be able to verify the
@@ -361,6 +333,14 @@ func (c OpenIdAuthController) redirectToAuthServerHandler(w http.ResponseWriter,
 		return
 	}
 
+	// Kiali only supports the authorization code flow.
+	if !isOpenIdCodeFlowPossible() {
+		w.Header().Set("Content-Type", "text/plain")
+		w.WriteHeader(http.StatusNotImplemented)
+		_, _ = w.Write([]byte("Cannot start authentication because it is not possible to use OpenId's authorization code flow. Check Kiali logs for more details."))
+		return
+	}
+
 	// Build scopes string
 	scopes := strings.Join(getConfiguredOpenIdScopes(), " ")
 
@@ -418,13 +398,8 @@ func (c OpenIdAuthController) redirectToAuthServerHandler(w http.ResponseWriter,
 	// the Kiali's signing key).
 	csrfHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", nonceCode, nowTime.UTC().Format("060102150405"), config.GetSigningKey())))
 
-	// Use OpenId's "implicit flow" by default. Use "authorization code" flow if possible.
-	responseType := "id_token"
-	if isOpenIdCodeFlowPossible() {
-		responseType = "code"
-	}
-
 	// Send redirection to browser
+	responseType := "code" // Request for the "authorization code" flow
 	redirectUri := fmt.Sprintf("%s?client_id=%s&response_type=%s&redirect_uri=%s&scope=%s&nonce=%s&state=%s",
 		authorizationEndpoint,
 		url.QueryEscape(conf.Auth.OpenId.ClientId),
@@ -558,7 +533,6 @@ func (p *openidFlowHelper) extractOpenIdCallbackParams(r *http.Request) *openidF
 	} else {
 		// Read relevant form data parameters
 		p.Code = r.Form.Get("code")
-		p.IdToken = r.Form.Get("id_token")
 		p.State = r.Form.Get("state")
 	}
 
@@ -567,35 +541,6 @@ func (p *openidFlowHelper) extractOpenIdCallbackParams(r *http.Request) *openidF
 	return p
 }
 
-// checkOpenIdImplicitFlowParams verifies that the callback parameters for the implicit flow
-// are all present, as required by Kiali.
-func (p *openidFlowHelper) checkOpenIdImplicitFlowParams() *openidFlowHelper {
-	// Do nothing if there was an error in previous flow steps.
-	if p.Error != nil {
-		return p
-	}
-
-	var validationError string
-	if p.NonceHash == nil {
-		validationError = "No nonce code present. Login window timed out."
-	}
-	if p.State == "" {
-		validationError = "State parameter is empty or invalid."
-	}
-	if p.IdToken == "" {
-		validationError = "Token is empty or invalid."
-	}
-
-	if len(validationError) != 0 {
-		p.Error = &AuthenticationFailureError{
-			HttpStatus: http.StatusBadRequest,
-			Reason:     validationError,
-		}
-	}
-
-	return p
-}
-
 // checkOpenIdAuthorizationCodeFlowParams verifies that the callback parameters for the authorization
 // code flow are all present, as required by Kiali.
 func (p *openidFlowHelper) checkOpenIdAuthorizationCodeFlowParams() *openidFlowHelper {
@@ -646,8 +591,8 @@ func (p *openidFlowHelper) checkAllowedDomains() *openidFlowHelper {
 // depending if RBAC is enabled.
 //
 // If RBAC is enabled, either the id_token or the access_token (as specified by the api_token in
-// the config) is tested agains the cluster API to check if the user has enough privileges
-// to login to Kiali.
+// the config) is tested against the cluster API to check if the user has enough privileges
+// to log in to Kiali.
 //
 // If RBAC is disabled, then only validity of the id_token is verified (see validateOpenIdTokenInHouse).
 func (p *openidFlowHelper) checkUserPrivileges() *openidFlowHelper {
@@ -1034,6 +979,8 @@ func isOpenIdCodeFlowPossible() bool {
 		}
 	}
 
+	log.Warning("Cannot use the authorization code flow because the OpenID provider does not support the 'code' response type")
+
 	return false
 }
 
diff --git a/business/checkers/authorization/principals_checker.go b/business/checkers/authorization/principals_checker.go
index cd4e3b934..3cfc7eaf8 100644
--- a/business/checkers/authorization/principals_checker.go
+++ b/business/checkers/authorization/principals_checker.go
@@ -2,6 +2,8 @@ package authorization
 
 import (
 	"fmt"
+	"regexp"
+	"strings"
 
 	api_security_v1beta "istio.io/api/security/v1beta1"
 	security_v1beta "istio.io/client-go/pkg/apis/security/v1beta1"
@@ -14,6 +16,10 @@ type PrincipalsChecker struct {
 	ServiceAccounts     []string
 }
 
+const (
+	wildCardMatch = "*"
+)
+
 func (pc PrincipalsChecker) Check() ([]*models.IstioCheck, bool) {
 	checks, valid := make([]*models.IstioCheck, 0), true
 
@@ -63,11 +69,31 @@ func (pc PrincipalsChecker) validateFromField(ruleIdx int, from []*api_security_
 }
 
 func (pc PrincipalsChecker) hasMatchingServiceAccount(principal string) bool {
+	if principal == wildCardMatch {
+		return true
+	}
 	for _, sa := range pc.ServiceAccounts {
-		if sa == principal {
+		if (strings.HasPrefix(principal, wildCardMatch) || strings.HasSuffix(principal, wildCardMatch)) && regexpFromPrincipal(principal).MatchString(sa) {
+			// Prefix match: “abc*” will match on value “abc” and “abcd”.
+			// Suffix match: “*abc” will match on value “abc” and “xabc”.
+			return true
+		} else if sa == principal {
 			return true
 		}
 	}
 
 	return false
 }
+
+func regexpFromPrincipal(principal string) *regexp.Regexp {
+	// Replace '*' from principal with regexp '.*'
+	escaped := strings.Replace(principal, "*", ".*", -1)
+
+	// We anchor the beginning and end of the string when it's
+	// to be used as a regex, so that we don't get spurious
+	// substring matches, e.g., "example.com" matching
+	// "foo.example.com".
+	anchored := strings.Join([]string{"^", escaped, "$"}, "")
+
+	return regexp.MustCompile(anchored)
+}
diff --git a/business/checkers/destination_rules_checker.go b/business/checkers/destination_rules_checker.go
index bc066f1e6..9f09139e1 100644
--- a/business/checkers/destination_rules_checker.go
+++ b/business/checkers/destination_rules_checker.go
@@ -36,10 +36,7 @@ func (in DestinationRulesChecker) runGroupChecks() models.IstioValidations {
 		destinationrules.MultiMatchChecker{Namespaces: in.Namespaces, ServiceEntries: seHosts, DestinationRules: in.DestinationRules},
 	}
 
-	// Appending validations that only applies to non-autoMTLS meshes
-	if !in.MTLSDetails.EnabledAutoMtls {
-		enabledDRCheckers = append(enabledDRCheckers, destinationrules.TrafficPolicyChecker{DestinationRules: in.DestinationRules, MTLSDetails: in.MTLSDetails})
-	}
+	enabledDRCheckers = append(enabledDRCheckers, destinationrules.TrafficPolicyChecker{DestinationRules: in.DestinationRules, MTLSDetails: in.MTLSDetails})
 
 	for _, checker := range enabledDRCheckers {
 		validations = validations.MergeValidations(checker.Check())
@@ -68,11 +65,8 @@ func (in DestinationRulesChecker) runChecks(destinationRule *networking_v1beta1.
 		common.ExportToNamespaceChecker{ExportTo: destinationRule.Spec.ExportTo, Namespaces: in.Namespaces},
 	}
 
-	// Appending validations that only applies to non-autoMTLS meshes
-	if !in.MTLSDetails.EnabledAutoMtls {
-		enabledCheckers = append(enabledCheckers, destinationrules.NamespaceWideMTLSChecker{DestinationRule: destinationRule, MTLSDetails: in.MTLSDetails})
-		enabledCheckers = append(enabledCheckers, destinationrules.MeshWideMTLSChecker{DestinationRule: destinationRule, MTLSDetails: in.MTLSDetails})
-	}
+	enabledCheckers = append(enabledCheckers, destinationrules.NamespaceWideMTLSChecker{DestinationRule: destinationRule, MTLSDetails: in.MTLSDetails})
+	enabledCheckers = append(enabledCheckers, destinationrules.MeshWideMTLSChecker{DestinationRule: destinationRule, MTLSDetails: in.MTLSDetails})
 
 	for _, checker := range enabledCheckers {
 		checks, validChecker := checker.Check()
diff --git a/business/checkers/k8sgateway_checker.go b/business/checkers/k8sgateway_checker.go
new file mode 100644
index 000000000..b19571095
--- /dev/null
+++ b/business/checkers/k8sgateway_checker.go
@@ -0,0 +1,46 @@
+package checkers
+
+import (
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/business/checkers/k8sgateways"
+	"github.com/kiali/kiali/models"
+)
+
+const K8sGatewayCheckerType = "k8sgateway"
+
+type K8sGatewayChecker struct {
+	K8sGateways []*k8s_networking_v1beta1.Gateway
+}
+
+// Check runs checks for the all namespaces actions as well as for the single namespace validations
+func (g K8sGatewayChecker) Check() models.IstioValidations {
+	// Multinamespace checkers
+	validations := k8sgateways.MultiMatchChecker{
+		K8sGateways: g.K8sGateways,
+	}.Check()
+
+	for _, gw := range g.K8sGateways {
+		validations.MergeValidations(g.runSingleChecks(gw))
+	}
+
+	return validations
+}
+
+func (g K8sGatewayChecker) runSingleChecks(gw *k8s_networking_v1beta1.Gateway) models.IstioValidations {
+	key, validations := EmptyValidValidation(gw.Name, gw.Namespace, K8sGatewayCheckerType)
+
+	enabledCheckers := []Checker{
+		k8sgateways.StatusChecker{
+			K8sGateway: gw,
+		},
+	}
+
+	for _, checker := range enabledCheckers {
+		checks, validChecker := checker.Check()
+		validations.Checks = append(validations.Checks, checks...)
+		validations.Valid = validations.Valid && validChecker
+	}
+
+	return models.IstioValidations{key: validations}
+}
diff --git a/business/checkers/k8sgateways/multi_match_checker.go b/business/checkers/k8sgateways/multi_match_checker.go
new file mode 100644
index 000000000..fccee4746
--- /dev/null
+++ b/business/checkers/k8sgateways/multi_match_checker.go
@@ -0,0 +1,106 @@
+package k8sgateways
+
+import (
+	"fmt"
+
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/models"
+)
+
+type MultiMatchChecker struct {
+	K8sGateways []*k8s_networking_v1beta1.Gateway
+}
+
+const (
+	K8sGatewayCheckerType = "k8sgateway"
+)
+
+// Check validates that no two gateways share the same host+port combination
+func (m MultiMatchChecker) Check() models.IstioValidations {
+	validations := models.IstioValidations{}
+
+	for _, g := range m.K8sGateways {
+		gatewayRuleName := g.Name
+		gatewayNamespace := g.Namespace
+
+		// With addresses
+		for _, address := range g.Spec.Addresses {
+			duplicate, _ := m.findMatchIP(address, g.Name)
+			if duplicate {
+				// The above is referenced by each one below..
+				currentHostValidation := createError(gatewayRuleName, "k8sgateways.multimatch.ip", gatewayNamespace, "spec/addresses/value")
+				validations = validations.MergeValidations(currentHostValidation)
+			}
+		}
+
+		// With listeners
+		for index, listener := range g.Spec.Listeners {
+			duplicate, _ := m.findMatch(listener, g.Name)
+			// Find in a different k8s GW
+			if duplicate {
+				// The above is referenced by each one below..
+				currentHostValidation := createError(gatewayRuleName, "k8sgateways.multimatch.listener", gatewayNamespace, fmt.Sprintf("spec/listeners[%d]/hostname", index))
+				validations = validations.MergeValidations(currentHostValidation)
+			}
+			// Check for unique listeners in the GW
+			for i, l := range g.Spec.Listeners {
+				if listener.Name != l.Name && l.Hostname != nil && listener.Hostname != nil && *listener.Hostname == *l.Hostname && listener.Port == l.Port && listener.Protocol == l.Protocol {
+					currentHostValidation := createError(gatewayRuleName, "k8sgateways.unique.listener", gatewayNamespace, fmt.Sprintf("spec/listeners[%d]/name", i))
+					validations = validations.MergeValidations(currentHostValidation)
+				}
+			}
+		}
+	}
+
+	return validations
+}
+
+// Create validation error for k8sgateway object
+func createError(gatewayRuleName string, ruleCode string, namespace string, path string) models.IstioValidations {
+	key := models.IstioValidationKey{Name: gatewayRuleName, Namespace: namespace, ObjectType: K8sGatewayCheckerType}
+	checks := models.Build(ruleCode, path)
+	rrValidation := &models.IstioValidation{
+		Name:       gatewayRuleName,
+		ObjectType: K8sGatewayCheckerType,
+		Valid:      true,
+		Checks: []*models.IstioCheck{
+			&checks,
+		},
+	}
+
+	return models.IstioValidations{key: rrValidation}
+}
+
+// findMatch uses a linear search with regexp to check for matching gateway host + port combinations. If this becomes a bottleneck for performance, replace with a graph or trie algorithm.
+func (m MultiMatchChecker) findMatch(listener k8s_networking_v1beta1.Listener, gwName string) (bool, []k8s_networking_v1beta1.Listener) {
+	duplicates := make([]k8s_networking_v1beta1.Listener, 0)
+	for _, gw := range m.K8sGateways {
+		if gw.Name == gwName {
+			continue
+		}
+		for _, l := range gw.Spec.Listeners {
+			if l.Hostname != nil && listener.Hostname != nil && *l.Hostname == *listener.Hostname && l.Port == listener.Port && l.Protocol == listener.Protocol {
+				duplicates = append(duplicates, listener)
+			}
+		}
+
+	}
+	return len(duplicates) > 0, duplicates
+}
+
+// Check duplicates IP
+func (m MultiMatchChecker) findMatchIP(address k8s_networking_v1beta1.GatewayAddress, gwName string) (bool, []k8s_networking_v1beta1.GatewayAddress) {
+	duplicates := make([]k8s_networking_v1beta1.GatewayAddress, 0)
+	for _, aa := range m.K8sGateways {
+		if aa.Name == gwName {
+			continue
+		}
+		for _, a := range aa.Spec.Addresses {
+			if a.Type != nil && address.Type != nil && *a.Type == *address.Type && a.Value == address.Value {
+				duplicates = append(duplicates, address)
+			}
+		}
+	}
+	return len(duplicates) > 0, duplicates
+}
diff --git a/business/checkers/k8sgateways/status_checker.go b/business/checkers/k8sgateways/status_checker.go
new file mode 100644
index 000000000..98bd1c55c
--- /dev/null
+++ b/business/checkers/k8sgateways/status_checker.go
@@ -0,0 +1,68 @@
+package k8sgateways
+
+import (
+	"fmt"
+
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/models"
+)
+
+type StatusChecker struct {
+	K8sGateway *k8s_networking_v1beta1.Gateway
+}
+
+type K8sGatewayStatus struct {
+	ObjectField string
+	Status      string
+}
+
+const GwAPICheckerCode string = "GWAPI"
+
+// K8sGatewayConditionStatus represents the status failures for a Condition in a K8sGateway
+var K8sGatewayConditionStatus = map[string]string{
+	"Scheduled": "False",
+	"Ready":     "False",
+}
+
+// K8sGatewayConditionStatus represents the status failures for a Condition in a K8sGateway
+var K8sGatewayListenersStatus = map[string]string{
+	"Conflicted":   "True",
+	"Detached":     "True",
+	"Ready":        "False",
+	"ResolvedRefs": "False",
+}
+
+// Check validates that no two gateways share the same host+port combination
+func (m StatusChecker) Check() ([]*models.IstioCheck, bool) {
+	validations := make([]*models.IstioCheck, 0)
+
+	for i, c := range m.K8sGateway.Status.Conditions {
+		if K8sGatewayConditionStatus[c.Type] == string(c.Status) {
+			check := createGwChecker(fmt.Sprintf("%s. GWAPI errors should be changed in the spec.", c.Message), fmt.Sprintf("status/conditions[%d]/reason/%s", i, c.Reason))
+			validations = append(validations, &check)
+		}
+	}
+
+	for i, l := range m.K8sGateway.Status.Listeners {
+		for _, c := range l.Conditions {
+			if K8sGatewayListenersStatus[c.Type] == string(c.Status) {
+				check := createGwChecker(fmt.Sprintf("%s. GWAPI errors should be changed in the spec.", c.Message), fmt.Sprintf("status/conditions[%d]/type/%s", i, c.Reason))
+				validations = append(validations, &check)
+			}
+		}
+	}
+
+	return validations, len(validations) == 0
+}
+
+// Create checker for GW validation (Gateway status)
+func createGwChecker(msg string, path string) models.IstioCheck {
+	check := models.IstioCheck{
+		Code:     GwAPICheckerCode,
+		Message:  msg,
+		Severity: models.WarningSeverity,
+		Path:     path,
+	}
+	return check
+}
diff --git a/business/checkers/k8shttproute_checker.go b/business/checkers/k8shttproute_checker.go
new file mode 100644
index 000000000..d86bb82cc
--- /dev/null
+++ b/business/checkers/k8shttproute_checker.go
@@ -0,0 +1,64 @@
+package checkers
+
+import (
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/business/checkers/k8shttproutes"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/models"
+)
+
+const K8sHTTPRouteCheckerType = "k8shttproute"
+
+type K8sHTTPRouteChecker struct {
+	K8sHTTPRoutes    []*k8s_networking_v1beta1.HTTPRoute
+	K8sGateways      []*k8s_networking_v1beta1.Gateway
+	Namespaces       models.Namespaces
+	RegistryServices []*kubernetes.RegistryService
+}
+
+// Check runs checks for the all namespaces actions as well as for the single namespace validations
+func (in K8sHTTPRouteChecker) Check() models.IstioValidations {
+	validations := models.IstioValidations{}
+
+	validations = validations.MergeValidations(in.runIndividualChecks())
+
+	return validations
+}
+
+// Runs individual checks for each HTTP Route
+func (in K8sHTTPRouteChecker) runIndividualChecks() models.IstioValidations {
+	validations := models.IstioValidations{}
+
+	gatewayNames := kubernetes.K8sGatewayNames(in.K8sGateways)
+
+	for _, rt := range in.K8sHTTPRoutes {
+		validations.MergeValidations(in.runChecks(rt, gatewayNames))
+	}
+
+	return validations
+}
+
+func (in K8sHTTPRouteChecker) runChecks(rt *k8s_networking_v1beta1.HTTPRoute, gatewayNames map[string]struct{}) models.IstioValidations {
+	key, validations := EmptyValidValidation(rt.Name, rt.Namespace, K8sHTTPRouteCheckerType)
+
+	enabledCheckers := []Checker{
+		k8shttproutes.NoK8sGatewayChecker{
+			K8sHTTPRoute: rt,
+			GatewayNames: gatewayNames,
+		},
+		k8shttproutes.NoHostChecker{
+			K8sHTTPRoute:     rt,
+			Namespaces:       in.Namespaces,
+			RegistryServices: in.RegistryServices,
+		},
+	}
+
+	for _, checker := range enabledCheckers {
+		checks, validChecker := checker.Check()
+		validations.Checks = append(validations.Checks, checks...)
+		validations.Valid = validations.Valid && validChecker
+	}
+
+	return models.IstioValidations{key: validations}
+}
diff --git a/business/checkers/k8shttproutes/no_host_checker.go b/business/checkers/k8shttproutes/no_host_checker.go
new file mode 100644
index 000000000..319809116
--- /dev/null
+++ b/business/checkers/k8shttproutes/no_host_checker.go
@@ -0,0 +1,48 @@
+package k8shttproutes
+
+import (
+	"fmt"
+
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/models"
+)
+
+type NoHostChecker struct {
+	Namespaces       models.Namespaces
+	K8sHTTPRoute     *k8s_networking_v1beta1.HTTPRoute
+	RegistryServices []*kubernetes.RegistryService
+}
+
+func (n NoHostChecker) Check() ([]*models.IstioCheck, bool) {
+	validations := make([]*models.IstioCheck, 0)
+	valid := true
+
+	for k, httpRoute := range n.K8sHTTPRoute.Spec.Rules {
+		for i, ref := range httpRoute.BackendRefs {
+			if ref.Kind == nil || string(*ref.Kind) != "Service" {
+				continue
+			}
+			namespace := n.K8sHTTPRoute.Namespace
+			if ref.Namespace != nil && string(*ref.Namespace) != "" {
+				namespace = string(*ref.Namespace)
+			}
+			fqdn := kubernetes.GetHost(string(ref.Name), namespace, n.Namespaces.GetNames())
+			if !n.checkDestination(fqdn.String(), namespace) {
+				path := fmt.Sprintf("spec/rules[%d]/backendRefs[%d]/name", k, i)
+				validation := models.Build("k8shttproutes.nohost.namenotfound", path)
+				validations = append(validations, &validation)
+				valid = false
+			}
+		}
+	}
+
+	return validations, valid
+}
+
+func (n NoHostChecker) checkDestination(sHost string, itemNamespace string) bool {
+	// Use RegistryService to check destinations that may not be covered with previous check
+	// i.e. Multi-cluster or Federation validations
+	return kubernetes.HasMatchingRegistryService(itemNamespace, sHost, n.RegistryServices)
+}
diff --git a/business/checkers/k8shttproutes/no_k8sgateway_checker.go b/business/checkers/k8shttproutes/no_k8sgateway_checker.go
new file mode 100644
index 000000000..8f749796e
--- /dev/null
+++ b/business/checkers/k8shttproutes/no_k8sgateway_checker.go
@@ -0,0 +1,55 @@
+package k8shttproutes
+
+import (
+	"fmt"
+
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/models"
+)
+
+type NoK8sGatewayChecker struct {
+	K8sHTTPRoute *k8s_networking_v1beta1.HTTPRoute
+	GatewayNames map[string]struct{}
+}
+
+// Check validates that the HTTPRoute is pointing to an existing Gateway
+func (s NoK8sGatewayChecker) Check() ([]*models.IstioCheck, bool) {
+	validations := make([]*models.IstioCheck, 0)
+
+	valid := s.ValidateHTTPRouteGateways(&validations)
+
+	return validations, valid
+}
+
+// ValidateHTTPRouteGateways checks all HTTPRoute gateways and checks that they're found from the given list of gatewayNames. Also return index of missing gatways to show clearer error path in editor
+func (s NoK8sGatewayChecker) ValidateHTTPRouteGateways(validations *[]*models.IstioCheck) bool {
+	valid := true
+
+	if len(s.K8sHTTPRoute.Spec.ParentRefs) > 0 {
+		for index, parentRef := range s.K8sHTTPRoute.Spec.ParentRefs {
+			if string(parentRef.Name) != "" && string(*parentRef.Kind) == kubernetes.K8sActualGatewayType && string(*parentRef.Group) == kubernetes.K8sNetworkingGroupVersionV1Beta1.Group {
+				namespace := s.K8sHTTPRoute.Namespace
+				if parentRef.Namespace != nil && string(*parentRef.Namespace) != "" {
+					namespace = string(*parentRef.Namespace)
+				}
+				valid = s.checkGateway(string(parentRef.Name), namespace, validations, fmt.Sprintf("spec/parentRefs[%d]/name/%s", index, string(parentRef.Name))) && valid
+			}
+		}
+	}
+	return valid
+}
+
+func (s NoK8sGatewayChecker) checkGateway(name, namespace string, validations *[]*models.IstioCheck, location string) bool {
+	hostname := kubernetes.ParseGatewayAsHost(name, namespace)
+	for gw := range s.GatewayNames {
+		gwHostname := kubernetes.ParseHost(gw, namespace)
+		if found := kubernetes.FilterByHost(hostname.String(), hostname.Namespace, gw, gwHostname.Namespace); found {
+			return true
+		}
+	}
+	validation := models.Build("k8shttproutes.nok8sgateway", location)
+	*validations = append(*validations, &validation)
+	return false
+}
diff --git a/business/checkers/peer_authentication_checker.go b/business/checkers/peer_authentication_checker.go
index a90ae582a..4ccd0dd37 100644
--- a/business/checkers/peer_authentication_checker.go
+++ b/business/checkers/peer_authentication_checker.go
@@ -48,16 +48,13 @@ func (m PeerAuthenticationChecker) runChecks(peerAuthn *security_v1beta.PeerAuth
 		enabledCheckers = append(enabledCheckers, peerauthentications.DisabledNamespaceWideChecker{PeerAuthn: peerAuthn, DestinationRules: m.MTLSDetails.DestinationRules})
 	}
 
-	// MeshWide and NamespaceWide validations are only needed with autoMtls disabled
-	if !m.MTLSDetails.EnabledAutoMtls {
-		// PeerAuthentications into  the root namespace namespace are considered Mesh-wide objects
-		if config.IsRootNamespace(peerAuthn.Namespace) {
-			enabledCheckers = append(enabledCheckers,
-				peerauthentications.MeshMtlsChecker{MeshPolicy: peerAuthn, MTLSDetails: m.MTLSDetails, IsServiceMesh: false})
-		} else {
-			enabledCheckers = append(enabledCheckers,
-				peerauthentications.NamespaceMtlsChecker{PeerAuthn: peerAuthn, MTLSDetails: m.MTLSDetails})
-		}
+	// PeerAuthentications into  the root namespace namespace are considered Mesh-wide objects
+	if config.IsRootNamespace(peerAuthn.Namespace) {
+		enabledCheckers = append(enabledCheckers,
+			peerauthentications.MeshMtlsChecker{MeshPolicy: peerAuthn, MTLSDetails: m.MTLSDetails, IsServiceMesh: false})
+	} else {
+		enabledCheckers = append(enabledCheckers,
+			peerauthentications.NamespaceMtlsChecker{PeerAuthn: peerAuthn, MTLSDetails: m.MTLSDetails})
 	}
 
 	for _, checker := range enabledCheckers {
diff --git a/business/health.go b/business/health.go
index da15f9ee5..832c973f0 100644
--- a/business/health.go
+++ b/business/health.go
@@ -7,7 +7,6 @@ import (
 	"github.com/prometheus/common/model"
 	"k8s.io/apimachinery/pkg/api/errors"
 
-	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/observability"
 	"github.com/kiali/kiali/prometheus"
@@ -16,7 +15,6 @@ import (
 // HealthService deals with fetching health from various sources and convert to kiali model
 type HealthService struct {
 	prom          prometheus.ClientInterface
-	k8s           kubernetes.ClientInterface
 	businessLayer *Layer
 }
 
diff --git a/business/istio_config.go b/business/istio_config.go
index abf358417..be65f91c7 100644
--- a/business/istio_config.go
+++ b/business/istio_config.go
@@ -16,7 +16,7 @@ import (
 	api_errors "k8s.io/apimachinery/pkg/api/errors"
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	api_types "k8s.io/apimachinery/pkg/types"
-	k8s_networking_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
@@ -142,8 +142,8 @@ func (in *IstioConfigService) GetIstioConfigList(ctx context.Context, criteria I
 		WasmPlugins:      []*extentions_v1alpha1.WasmPlugin{},
 		Telemetries:      []*v1alpha1.Telemetry{},
 
-		K8sGateways:   []*k8s_networking_v1alpha2.Gateway{},
-		K8sHTTPRoutes: []*k8s_networking_v1alpha2.HTTPRoute{},
+		K8sGateways:   []*k8s_networking_v1beta1.Gateway{},
+		K8sHTTPRoutes: []*k8s_networking_v1beta1.HTTPRoute{},
 
 		AuthorizationPolicies:  []*security_v1beta1.AuthorizationPolicy{},
 		PeerAuthentications:    []*security_v1beta1.PeerAuthentication{},
@@ -151,7 +151,7 @@ func (in *IstioConfigService) GetIstioConfigList(ctx context.Context, criteria I
 	}
 
 	// Use the Istio Registry when AllNamespaces is present
-	if criteria.AllNamespaces {
+	if criteria.AllNamespaces && config.Get().ExternalServices.Istio.IstioAPIEnabled {
 		registryCriteria := RegistryCriteria{
 			AllNamespaces: true,
 		}
@@ -214,11 +214,12 @@ func (in *IstioConfigService) GetIstioConfigList(ctx context.Context, criteria I
 
 		return istioConfigList, nil
 	}
-
-	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
-	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
-	if _, err := in.businessLayer.Namespace.GetNamespace(ctx, criteria.Namespace); err != nil {
-		return models.IstioConfigList{}, err
+	if !criteria.AllNamespaces {
+		// Check if user has access to the namespace (RBAC) in cache scenarios and/or
+		// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
+		if _, err := in.businessLayer.Namespace.GetNamespace(ctx, criteria.Namespace); err != nil {
+			return models.IstioConfigList{}, err
+		}
 	}
 
 	isWorkloadSelector := criteria.WorkloadSelector != ""
@@ -306,7 +307,7 @@ func (in *IstioConfigService) GetIstioConfigList(ctx context.Context, criteria I
 			}
 			// TODO gwl.Items, there is conflict itself in Gateway API between returned types referenced or not
 			//else {
-			//	if gwl, e := in.k8s.GatewayAPI().GatewayV1alpha2().Gateways(criteria.Namespace).List(ctx, listOpts); e == nil {
+			//	if gwl, e := in.k8s.GatewayAPI().GatewayV1beta1().Gateways(criteria.Namespace).List(ctx, listOpts); e == nil {
 			//		istioConfigList.K8sGateways = gwl.Items
 			//	}
 			//}
@@ -327,7 +328,7 @@ func (in *IstioConfigService) GetIstioConfigList(ctx context.Context, criteria I
 			}
 			// TODO gwl.Items, there is conflict itself in Gateway API between returned types referenced or not
 			//else {
-			//	if gwl, e := in.k8s.GatewayAPI().GatewayV1alpha2().HTTPRoutes(criteria.Namespace).List(ctx, listOpts); e == nil {
+			//	if gwl, e := in.k8s.GatewayAPI().GatewayV1beta1().HTTPRoutes(criteria.Namespace).List(ctx, listOpts); e == nil {
 			//		istioConfigList.K8sHTTPRoutes = gwl.Items
 			//	}
 			//}
@@ -600,16 +601,16 @@ func (in *IstioConfigService) GetIstioConfigDetails(ctx context.Context, namespa
 			istioConfigDetail.Gateway.APIVersion = kubernetes.ApiNetworkingVersionV1Beta1
 		}
 	case kubernetes.K8sGateways:
-		istioConfigDetail.K8sGateway, err = in.k8s.GatewayAPI().GatewayV1alpha2().Gateways(namespace).Get(ctx, object, getOpts)
+		istioConfigDetail.K8sGateway, err = in.k8s.GatewayAPI().GatewayV1beta1().Gateways(namespace).Get(ctx, object, getOpts)
 		if err == nil {
 			istioConfigDetail.K8sGateway.Kind = kubernetes.K8sActualGatewayType
-			istioConfigDetail.K8sGateway.APIVersion = kubernetes.K8sApiNetworkingVersionV1Alpha2
+			istioConfigDetail.K8sGateway.APIVersion = kubernetes.K8sApiNetworkingVersionV1Beta1
 		}
 	case kubernetes.K8sHTTPRoutes:
-		istioConfigDetail.K8sHTTPRoute, err = in.k8s.GatewayAPI().GatewayV1alpha2().HTTPRoutes(namespace).Get(ctx, object, getOpts)
+		istioConfigDetail.K8sHTTPRoute, err = in.k8s.GatewayAPI().GatewayV1beta1().HTTPRoutes(namespace).Get(ctx, object, getOpts)
 		if err == nil {
 			istioConfigDetail.K8sHTTPRoute.Kind = kubernetes.K8sActualHTTPRouteType
-			istioConfigDetail.K8sHTTPRoute.APIVersion = kubernetes.K8sApiNetworkingVersionV1Alpha2
+			istioConfigDetail.K8sHTTPRoute.APIVersion = kubernetes.K8sApiNetworkingVersionV1Beta1
 		}
 	case kubernetes.ServiceEntries:
 		istioConfigDetail.ServiceEntry, err = in.k8s.Istio().NetworkingV1beta1().ServiceEntries(namespace).Get(ctx, object, getOpts)
@@ -747,7 +748,7 @@ func (in *IstioConfigService) GetIstioConfigDetailsFromRegistry(ctx context.Cont
 			if cfg.Name == object && cfg.Namespace == namespace {
 				istioConfigDetail.K8sGateway = cfg
 				istioConfigDetail.K8sGateway.Kind = kubernetes.K8sGatewayType
-				istioConfigDetail.K8sGateway.APIVersion = kubernetes.K8sApiNetworkingVersionV1Alpha2
+				istioConfigDetail.K8sGateway.APIVersion = kubernetes.K8sApiNetworkingVersionV1Beta1
 				return istioConfigDetail, nil
 			}
 		}
@@ -757,7 +758,7 @@ func (in *IstioConfigService) GetIstioConfigDetailsFromRegistry(ctx context.Cont
 			if cfg.Name == object && cfg.Namespace == namespace {
 				istioConfigDetail.K8sHTTPRoute = cfg
 				istioConfigDetail.K8sHTTPRoute.Kind = kubernetes.K8sHTTPRouteType
-				istioConfigDetail.K8sHTTPRoute.APIVersion = kubernetes.K8sApiNetworkingVersionV1Alpha2
+				istioConfigDetail.K8sHTTPRoute.APIVersion = kubernetes.K8sApiNetworkingVersionV1Beta1
 				return istioConfigDetail, nil
 			}
 		}
@@ -891,9 +892,9 @@ func (in *IstioConfigService) DeleteIstioConfigDetail(namespace, resourceType, n
 	case kubernetes.Gateways:
 		err = in.k8s.Istio().NetworkingV1beta1().Gateways(namespace).Delete(ctx, name, delOpts)
 	case kubernetes.K8sGateways:
-		err = in.k8s.GatewayAPI().GatewayV1alpha2().Gateways(namespace).Delete(ctx, name, delOpts)
+		err = in.k8s.GatewayAPI().GatewayV1beta1().Gateways(namespace).Delete(ctx, name, delOpts)
 	case kubernetes.K8sHTTPRoutes:
-		err = in.k8s.GatewayAPI().GatewayV1alpha2().HTTPRoutes(namespace).Delete(ctx, name, delOpts)
+		err = in.k8s.GatewayAPI().GatewayV1beta1().HTTPRoutes(namespace).Delete(ctx, name, delOpts)
 	case kubernetes.ServiceEntries:
 		err = in.k8s.Istio().NetworkingV1beta1().ServiceEntries(namespace).Delete(ctx, name, delOpts)
 	case kubernetes.Sidecars:
@@ -947,11 +948,11 @@ func (in *IstioConfigService) UpdateIstioConfigDetail(namespace, resourceType, n
 		istioConfigDetail.Gateway = &networking_v1beta1.Gateway{}
 		istioConfigDetail.Gateway, err = in.k8s.Istio().NetworkingV1beta1().Gateways(namespace).Patch(ctx, name, patchType, bytePatch, patchOpts)
 	case kubernetes.K8sGateways:
-		istioConfigDetail.K8sGateway = &k8s_networking_v1alpha2.Gateway{}
-		istioConfigDetail.K8sGateway, err = in.k8s.GatewayAPI().GatewayV1alpha2().Gateways(namespace).Patch(ctx, name, patchType, bytePatch, patchOpts)
+		istioConfigDetail.K8sGateway = &k8s_networking_v1beta1.Gateway{}
+		istioConfigDetail.K8sGateway, err = in.k8s.GatewayAPI().GatewayV1beta1().Gateways(namespace).Patch(ctx, name, patchType, bytePatch, patchOpts)
 	case kubernetes.K8sHTTPRoutes:
-		istioConfigDetail.K8sHTTPRoute = &k8s_networking_v1alpha2.HTTPRoute{}
-		istioConfigDetail.K8sHTTPRoute, err = in.k8s.GatewayAPI().GatewayV1alpha2().HTTPRoutes(namespace).Patch(ctx, name, patchType, bytePatch, patchOpts)
+		istioConfigDetail.K8sHTTPRoute = &k8s_networking_v1beta1.HTTPRoute{}
+		istioConfigDetail.K8sHTTPRoute, err = in.k8s.GatewayAPI().GatewayV1beta1().HTTPRoutes(namespace).Patch(ctx, name, patchType, bytePatch, patchOpts)
 	case kubernetes.ServiceEntries:
 		istioConfigDetail.ServiceEntry = &networking_v1beta1.ServiceEntry{}
 		istioConfigDetail.ServiceEntry, err = in.k8s.Istio().NetworkingV1beta1().ServiceEntries(namespace).Patch(ctx, name, patchType, bytePatch, patchOpts)
@@ -1025,19 +1026,19 @@ func (in *IstioConfigService) CreateIstioConfigDetail(namespace, resourceType st
 		}
 		istioConfigDetail.Gateway, err = in.k8s.Istio().NetworkingV1beta1().Gateways(namespace).Create(ctx, istioConfigDetail.Gateway, createOpts)
 	case kubernetes.K8sGateways:
-		istioConfigDetail.K8sGateway = &k8s_networking_v1alpha2.Gateway{}
+		istioConfigDetail.K8sGateway = &k8s_networking_v1beta1.Gateway{}
 		err = json.Unmarshal(body, istioConfigDetail.K8sGateway)
 		if err != nil {
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
-		istioConfigDetail.K8sGateway, err = in.k8s.GatewayAPI().GatewayV1alpha2().Gateways(namespace).Create(ctx, istioConfigDetail.K8sGateway, createOpts)
+		istioConfigDetail.K8sGateway, err = in.k8s.GatewayAPI().GatewayV1beta1().Gateways(namespace).Create(ctx, istioConfigDetail.K8sGateway, createOpts)
 	case kubernetes.K8sHTTPRoutes:
-		istioConfigDetail.K8sHTTPRoute = &k8s_networking_v1alpha2.HTTPRoute{}
+		istioConfigDetail.K8sHTTPRoute = &k8s_networking_v1beta1.HTTPRoute{}
 		err = json.Unmarshal(body, istioConfigDetail.K8sHTTPRoute)
 		if err != nil {
 			return istioConfigDetail, api_errors.NewBadRequest(err.Error())
 		}
-		istioConfigDetail.K8sHTTPRoute, err = in.k8s.GatewayAPI().GatewayV1alpha2().HTTPRoutes(namespace).Create(ctx, istioConfigDetail.K8sHTTPRoute, createOpts)
+		istioConfigDetail.K8sHTTPRoute, err = in.k8s.GatewayAPI().GatewayV1beta1().HTTPRoutes(namespace).Create(ctx, istioConfigDetail.K8sHTTPRoute, createOpts)
 	case kubernetes.ServiceEntries:
 		istioConfigDetail.ServiceEntry = &networking_v1beta1.ServiceEntry{}
 		err = json.Unmarshal(body, istioConfigDetail.ServiceEntry)
diff --git a/business/istio_status.go b/business/istio_status.go
index f5a9fd9e3..c64e76b53 100644
--- a/business/istio_status.go
+++ b/business/istio_status.go
@@ -29,7 +29,7 @@ func (iss *IstioStatusService) GetStatus(ctx context.Context) (kubernetes.IstioC
 	)
 	defer end()
 
-	if !config.Get().ExternalServices.Istio.ComponentStatuses.Enabled {
+	if !config.Get().ExternalServices.Istio.ComponentStatuses.Enabled || !config.Get().ExternalServices.Istio.IstioAPIEnabled {
 		return kubernetes.IstioComponentStatus{}, nil
 	}
 
diff --git a/business/istio_validations.go b/business/istio_validations.go
index 5f67d3ae8..468ea5854 100644
--- a/business/istio_validations.go
+++ b/business/istio_validations.go
@@ -78,11 +78,17 @@ func (in *IstioValidationsService) GetValidations(ctx context.Context, namespace
 	var rbacDetails kubernetes.RBACDetails
 	var registryServices []*kubernetes.RegistryService
 
-	wg.Add(4) // We need to add these here to make sure we don't execute wg.Wait() before scheduler has started goroutines
+	var istioApiEnabled = config.Get().ExternalServices.Istio.IstioAPIEnabled
+
+	wg.Add(3) // We need to add these here to make sure we don't execute wg.Wait() before scheduler has started goroutines
 	if service != "" {
 		wg.Add(1)
 	}
 
+	if istioApiEnabled {
+		wg.Add(1)
+	}
+
 	// We fetch without target service as some validations will require full-namespace details
 	go in.fetchIstioConfigList(ctx, &istioConfigList, &mtlsDetails, &rbacDetails, namespace, errChan, &wg)
 
@@ -92,11 +98,15 @@ func (in *IstioValidationsService) GetValidations(ctx context.Context, namespace
 	} else {
 		go in.fetchAllWorkloads(ctx, &workloadsPerNamespace, &namespaces, errChan, &wg)
 	}
+
 	go in.fetchNonLocalmTLSConfigs(&mtlsDetails, errChan, &wg)
 	if service != "" {
 		go in.fetchServices(ctx, &services, namespace, errChan, &wg)
 	}
-	go in.fetchRegistryServices(&registryServices, errChan, &wg)
+
+	if istioApiEnabled {
+		go in.fetchRegistryServices(&registryServices, errChan, &wg)
+	}
 
 	wg.Wait()
 	close(errChan)
@@ -137,8 +147,10 @@ func (in *IstioValidationsService) getAllObjectCheckers(istioConfigList models.I
 		checkers.SidecarChecker{Sidecars: istioConfigList.Sidecars, Namespaces: namespaces, WorkloadsPerNamespace: workloadsPerNamespace, ServiceEntries: istioConfigList.ServiceEntries, RegistryServices: registryServices},
 		checkers.RequestAuthenticationChecker{RequestAuthentications: istioConfigList.RequestAuthentications, WorkloadsPerNamespace: workloadsPerNamespace},
 		checkers.WorkloadChecker{AuthorizationPolicies: rbacDetails.AuthorizationPolicies, WorkloadsPerNamespace: workloadsPerNamespace},
+		checkers.K8sGatewayChecker{K8sGateways: istioConfigList.K8sGateways},
 		checkers.WasmPluginChecker{WasmPlugins: istioConfigList.WasmPlugins, Namespaces: namespaces},
 		checkers.TelemetryChecker{Telemetries: istioConfigList.Telemetries, Namespaces: namespaces},
+		checkers.K8sHTTPRouteChecker{K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes, K8sGateways: istioConfigList.K8sGateways, Namespaces: namespaces, RegistryServices: registryServices},
 	}
 }
 
@@ -164,6 +176,8 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 	var referenceChecker ReferenceChecker
 	istioReferences := models.IstioReferencesMap{}
 
+	var istioApiEnabled = config.Get().ExternalServices.Istio.IstioAPIEnabled
+
 	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
 	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
 	if _, err = in.businessLayer.Namespace.GetNamespace(ctx, namespace); err != nil {
@@ -178,11 +192,20 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 	errChan := make(chan error, 1)
 
 	// Get all the Istio objects from a Namespace and all gateways from every namespace
-	wg.Add(4)
+	wg.Add(3)
+
+	if istioApiEnabled {
+		wg.Add(1)
+	}
+
 	go in.fetchIstioConfigList(ctx, &istioConfigList, &mtlsDetails, &rbacDetails, namespace, errChan, &wg)
 	go in.fetchAllWorkloads(ctx, &workloadsPerNamespace, &namespaces, errChan, &wg)
 	go in.fetchNonLocalmTLSConfigs(&mtlsDetails, errChan, &wg)
-	go in.fetchRegistryServices(&registryServices, errChan, &wg)
+
+	if istioApiEnabled {
+		go in.fetchRegistryServices(&registryServices, errChan, &wg)
+	}
+
 	wg.Wait()
 
 	noServiceChecker := checkers.NoServiceChecker{Namespaces: namespaces, IstioConfigList: &istioConfigList, WorkloadsPerNamespace: workloadsPerNamespace, AuthorizationDetails: &rbacDetails, RegistryServices: registryServices, PolicyAllowAny: in.isPolicyAllowAny()}
@@ -232,13 +255,19 @@ func (in *IstioValidationsService) GetIstioObjectValidations(ctx context.Context
 	case kubernetes.EnvoyFilters:
 		// Validation on EnvoyFilters are not yet in place
 	case kubernetes.WasmPlugins:
-		// TODO
+		// Validation on WasmPlugins is not expected
 	case kubernetes.Telemetries:
-		// TODO
+		// Validation on Telemetries is not expected
 	case kubernetes.K8sGateways:
-		// TODO
+		// Validations on K8sGateways
+		objectCheckers = []ObjectChecker{
+			checkers.K8sGatewayChecker{K8sGateways: istioConfigList.K8sGateways},
+		}
+		referenceChecker = references.K8sGatewayReferences{K8sGateways: istioConfigList.K8sGateways, K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes}
 	case kubernetes.K8sHTTPRoutes:
-		// TODO
+		httpRouteChecker := checkers.K8sHTTPRouteChecker{K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes, K8sGateways: istioConfigList.K8sGateways, Namespaces: namespaces, RegistryServices: registryServices}
+		objectCheckers = []ObjectChecker{noServiceChecker, httpRouteChecker}
+		referenceChecker = references.K8sHTTPRouteReferences{K8sHTTPRoutes: istioConfigList.K8sHTTPRoutes, Namespaces: namespaces}
 	default:
 		err = fmt.Errorf("object type not found: %v", objectType)
 	}
@@ -372,6 +401,8 @@ func (in *IstioValidationsService) fetchIstioConfigList(ctx context.Context, rVa
 		IncludeWorkloadEntries:        true,
 		IncludeAuthorizationPolicies:  true,
 		IncludePeerAuthentications:    true,
+		IncludeK8sHTTPRoutes:          true,
+		IncludeK8sGateways:            true,
 	}
 	istioConfigList, err := in.businessLayer.IstioConfig.GetIstioConfigList(ctx, criteria)
 	if err != nil {
@@ -394,6 +425,12 @@ func (in *IstioValidationsService) fetchIstioConfigList(ctx context.Context, rVa
 	// All Gateways
 	rValue.Gateways = append(rValue.Gateways, kubernetes.FilterAutogeneratedGateways(istioConfigList.Gateways)...)
 
+	// All K8sGateways
+	rValue.K8sGateways = append(rValue.K8sGateways, kubernetes.FilterSupportedK8sGateways(istioConfigList.K8sGateways)...)
+
+	// All K8sHTTPRoutes
+	rValue.K8sHTTPRoutes = append(rValue.K8sHTTPRoutes, istioConfigList.K8sHTTPRoutes...)
+
 	// All Sidecars
 	rValue.Sidecars = append(rValue.Sidecars, istioConfigList.Sidecars...)
 
@@ -403,6 +440,9 @@ func (in *IstioValidationsService) fetchIstioConfigList(ctx context.Context, rVa
 	// All WorkloadEntries
 	rValue.WorkloadEntries = append(rValue.WorkloadEntries, istioConfigList.WorkloadEntries...)
 
+	// All K8sGateways
+	rValue.K8sGateways = append(rValue.K8sGateways, istioConfigList.K8sGateways...)
+
 	in.filterPeerAuths(namespace, mtlsDetails, istioConfigList.PeerAuthentications)
 
 	in.filterAuthPolicies(namespace, rbacDetails, istioConfigList.AuthorizationPolicies)
diff --git a/business/jaeger.go b/business/jaeger.go
index a4e79cb35..c0db39c11 100644
--- a/business/jaeger.go
+++ b/business/jaeger.go
@@ -138,6 +138,11 @@ func (in *JaegerService) GetAppTraces(ns, app string, query models.TracingQuery)
 	return r, nil
 }
 
+// GetServiceTraces returns traces involving the requested service.  Note that because the tracing API pulls traces by "App", only a
+// subset of the traces may actually involve the requested service.  Callers may need to upwardly adjust TracingQuery.Limit to get back
+// the number of desired traces.  It depends on the number of services backing the app. For example, if there are 2 services for the
+// app, if evenly distributed, a query limit of 20 may return only 10 traces.  The ratio is typically not as bad as it is with
+// GetWorkloadTraces.
 func (in *JaegerService) GetServiceTraces(ctx context.Context, ns, service string, query models.TracingQuery) (*jaeger.JaegerResponse, error) {
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetServiceTraces",
@@ -155,9 +160,7 @@ func (in *JaegerService) GetServiceTraces(ctx context.Context, ns, service strin
 		// No post-filtering
 		return in.GetAppTraces(ns, app, query)
 	}
-	// Now we're in context where app != service, so we need to perform post-filtering based on operation names
-	// Artificial increase of limit (see explanation in GetWorkloadTraces)
-	query.Limit *= 2
+
 	r, err := in.GetAppTraces(ns, app, query)
 	if r != nil && err == nil {
 		// Filter out app traces based on operation name.
@@ -177,6 +180,10 @@ func (in *JaegerService) GetServiceTraces(ctx context.Context, ns, service strin
 	return r, err
 }
 
+// GetWorkloadTraces returns traces involving the requested workload.  Note that because the tracing API pulls traces by "App", only
+// a subset of the traces may actually involve the requested workload.  Callers may need to upwardly adjust TracingQuery.Limit to get back
+// the number of desired traces.  It depends on the number of workloads backing the app. For example, if there are 5 workloads for the
+// app, if evenly distributed, a query limit of 25 may return only 5 traces.
 func (in *JaegerService) GetWorkloadTraces(ctx context.Context, ns, workload string, query models.TracingQuery) (*jaeger.JaegerResponse, error) {
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "GetWorkloadTraces",
@@ -190,11 +197,7 @@ func (in *JaegerService) GetWorkloadTraces(ctx context.Context, ns, workload str
 	if err != nil {
 		return nil, err
 	}
-	// Because Traces are fetched per App and not Workloads, the 'limit' query param will apply to app's traces, not workloads,
-	// so it will not be consistent with the final result. In other words, we ask for 15 traces but could very well end up with
-	// only 3 traces for the workload even if there's more.
-	// To try to attenuate this effect, we will artificially increase the limit, then cut it down after workload filtering.
-	query.Limit *= 5
+
 	r, err := in.GetAppTraces(ns, app, query)
 	// Filter out app traces based on the node_id tag, that contains workload information.
 	if r != nil && err == nil {
diff --git a/business/layer.go b/business/layer.go
index 7567e8086..acb62b232 100644
--- a/business/layer.go
+++ b/business/layer.go
@@ -14,7 +14,9 @@ import (
 	"github.com/kiali/kiali/prometheus"
 )
 
-// Layer is a container for fast access to inner services
+// Layer is a container for fast access to inner services.
+// A business layer is created per token/user. Any data that
+// needs to be saved across layers is saved in the Kiali Cache.
 type Layer struct {
 	App            AppService
 	Health         HealthService
@@ -37,9 +39,8 @@ type Layer struct {
 }
 
 // Global clientfactory and prometheus clients.
-var clientFactory kubernetes.ClientFactory
-
 var (
+	clientFactory    kubernetes.ClientFactory
 	jaegerClient     jaeger.ClientInterface
 	kialiCache       cache.KialiCache
 	once             sync.Once
@@ -55,6 +56,14 @@ func initKialiCache() {
 		}
 	}
 
+	userClient, err := kubernetes.GetClientFactory()
+	if err != nil {
+		log.Errorf("Failed to create client factory. Err: %s", err)
+		return
+	}
+	clientFactory = userClient
+
+	// TODO: Remove conditonal once cache is fully mandatory.
 	if config.Get().KubernetesConfig.CacheEnabled {
 		log.Infof("Initializing Kiali Cache")
 
@@ -88,7 +97,7 @@ func initKialiCache() {
 			}
 		}
 
-		cache, err := cache.NewKialiCache(namespaceSeedList...)
+		cache, err := cache.NewKialiCache(clientFactory, *config.Get(), namespaceSeedList...)
 		if err != nil {
 			log.Errorf("Error initializing Kiali Cache. Details: %s", err)
 			return
@@ -112,21 +121,12 @@ func IsResourceCached(namespace string, resource string) bool {
 }
 
 func Start() {
-	// Kiali Cache will be initialized once at first use of Business layer
+	// Kiali Cache will be initialized once at start up.
 	once.Do(initKialiCache)
 }
 
 // Get the business.Layer
 func Get(authInfo *api.AuthInfo) (*Layer, error) {
-	// Use an existing client factory if it exists, otherwise create and use in the future
-	if clientFactory == nil {
-		userClient, err := kubernetes.GetClientFactory()
-		if err != nil {
-			return nil, err
-		}
-		clientFactory = userClient
-	}
-
 	// Creates a new k8s client based on the current users token
 	k8s, err := clientFactory.GetClient(authInfo)
 	if err != nil {
@@ -165,11 +165,13 @@ func SetWithBackends(cf kubernetes.ClientFactory, prom prometheus.ClientInterfac
 	prometheusClient = prom
 }
 
-// NewWithBackends creates the business layer using the passed k8s and prom clients
+// NewWithBackends creates the business layer using the passed k8s and prom clients.
+// Note that the client passed here should *not* be the Kiali ServiceAccount client.
+// It should be the user client based on the logged in user's token.
 func NewWithBackends(k8s kubernetes.ClientInterface, prom prometheus.ClientInterface, jaegerClient JaegerLoader) *Layer {
 	temporaryLayer := &Layer{}
 	temporaryLayer.App = AppService{prom: prom, k8s: k8s, businessLayer: temporaryLayer}
-	temporaryLayer.Health = HealthService{prom: prom, k8s: k8s, businessLayer: temporaryLayer}
+	temporaryLayer.Health = HealthService{prom: prom, businessLayer: temporaryLayer}
 	temporaryLayer.IstioConfig = IstioConfigService{k8s: k8s, businessLayer: temporaryLayer}
 	temporaryLayer.IstioStatus = IstioStatusService{k8s: k8s, businessLayer: temporaryLayer}
 	temporaryLayer.IstioCerts = IstioCertsService{k8s: k8s, businessLayer: temporaryLayer}
@@ -186,7 +188,19 @@ func NewWithBackends(k8s kubernetes.ClientInterface, prom prometheus.ClientInter
 	temporaryLayer.TLS = TLSService{k8s: k8s, businessLayer: temporaryLayer}
 	temporaryLayer.TokenReview = NewTokenReview(k8s)
 	temporaryLayer.Validations = IstioValidationsService{k8s: k8s, businessLayer: temporaryLayer}
-	temporaryLayer.Workload = WorkloadService{k8s: k8s, prom: prom, businessLayer: temporaryLayer}
+
+	// TODO: Remove conditional once cache is fully mandatory.
+	if config.Get().KubernetesConfig.CacheEnabled {
+		// The caching client effectively uses two different SA account tokens.
+		// The kiali SA token is used for all cache methods. The cache methods are
+		// read-only. Methods that are not cached and methods that modify objects
+		// use the user's token through the normal client.
+		// TODO: Always pass caching client once caching is mandatory.
+		cachingClient := cache.NewCachingClient(kialiCache, k8s)
+		temporaryLayer.Workload = *NewWorkloadService(cachingClient, prom, kialiCache, temporaryLayer, config.Get())
+	} else {
+		temporaryLayer.Workload = *NewWorkloadService(k8s, prom, kialiCache, temporaryLayer, config.Get())
+	}
 
 	return temporaryLayer
 }
diff --git a/business/mesh.go b/business/mesh.go
index ad57f5457..707555004 100644
--- a/business/mesh.go
+++ b/business/mesh.go
@@ -3,6 +3,7 @@ package business
 import (
 	"context"
 	"encoding/json"
+	stderrors "errors"
 	"fmt"
 	"net/http"
 	"os"
@@ -60,9 +61,6 @@ type Cluster struct {
 
 	// Network specifies the logical NETWORK_ID as known by the Control Plane
 	Network string `json:"network"`
-
-	// SecretName is the name of the kubernetes "remote secret" where data of this cluster was resolved
-	SecretName string `json:"secretName"`
 }
 
 // KialiInstance represents a Kiali installation. It holds some data about
@@ -293,7 +291,6 @@ func (in *MeshService) ResolveKialiControlPlaneCluster(r *http.Request) (*Cluste
 		KialiInstances:       kialiInstances,
 		Name:                 myClusterName,
 		Network:              kialiNetwork,
-		SecretName:           "",
 	}
 
 	return kialiControlPlaneCluster, nil
@@ -458,12 +455,13 @@ func (in *MeshService) resolveKialiNetwork() (string, error) {
 	return typedNetworkConfig, nil
 }
 
+// Defines where the files are located that contain the remote cluster secrets
+var remoteClusterSecretsDir = "/kiali-remote-cluster-secrets"
+
 // resolveRemoteClustersFromSecrets resolves the metadata about "other" clusters that are
 // visible to the adjacent mesh control plane. This assumes that the Istio namespace is
 // named the same as in Kiali's Cluster.
 func (in *MeshService) resolveRemoteClustersFromSecrets() ([]Cluster, error) {
-	conf := config.Get()
-
 	// For the ControlPlane to be able to "see" remote clusters, some "remote secrets" need to be in
 	// place. These remote secrets contain <kubeconfig files> that the ControlPlane uses to
 	// query the remote clusters. Without them, the control plane is not capable of pushing traffic
@@ -474,54 +472,68 @@ func (in *MeshService) resolveRemoteClustersFromSecrets() ([]Cluster, error) {
 	// Strictly speaking, this list may be incomplete: it's list of visible clusters for a control plane.
 	// But, for now, let's use it as the absolute "list of clusters in the mesh (excluding home cluster)".
 
-	// "Remote secrets" are created using the command `istioctl x create-remote-secret` which
-	// labels the secrets with istio/multiCluster=true. Let's use that label to fetch the secrets of interest.
-	secrets, err := in.k8s.GetSecrets(conf.IstioNamespace, "istio/multiCluster=true")
-	if err != nil {
-		if errors.IsForbidden(err) {
-			// A forbidden error means that we don't have privileges to list secrets in the Istio namespace.
-			// This may be because sysadmin may not want us to do that for security and probably
-			// because it is known that the environment is a single-cluster. So, return
-			// and empty list of clusters, avoid the warning error and use a trace log message.
-			log.Trace("Not enough privileges to list secrets with istio/multiCluster=true label.")
-			return []Cluster{}, nil
+	// Remote cluster secrets are mounted on the file system by the Kiali installer under the "/kiali-remote-cluster-secrets" directory.
+	// Each cluster has its own subdirectory, usually named the same as the cluster (though not necessarily). e.g. "/kiali-remote-cluster-secrets/<cluster name>"
+	// Cluster configs are found in a file whose name is the cluster name; e.g. "/kiali-remote-cluster-secrets/<cluster name>/<cluster name>"
+	// So walk the directory "/kiali-remote-cluster-secrets" to obtain all known remote cluster configs.
+
+	remoteClusterSecrets := make(map[string][]byte, 0)
+
+	secretDirs, err := os.ReadDir(remoteClusterSecretsDir)
+	if err == nil {
+		for _, sd := range secretDirs {
+			secretAbsDir := remoteClusterSecretsDir + "/" + sd.Name()
+			secretFiles, err := os.ReadDir(secretAbsDir)
+			if err == nil {
+				for _, sf := range secretFiles {
+					clusterName := sf.Name()
+					secretAbsFile := secretAbsDir + "/" + clusterName
+					statinfo, staterr := os.Stat(secretAbsFile)
+					if statinfo.IsDir() || staterr != nil {
+						continue // we only want to process readable files - we are not interested in other files that get mounted here
+					}
+					b, err := os.ReadFile(secretAbsFile)
+					if err == nil {
+						if len(b) > 0 {
+							remoteClusterSecrets[clusterName] = b
+							log.Debugf("Remote cluster credentials loaded from secret file [%s]", secretAbsFile)
+						} else {
+							log.Errorf("The credentials were empty in remote cluster secret file [%s]", secretAbsFile)
+						}
+					} else {
+						log.Errorf("Failed to read remote cluster secret file [%s]: %v", secretAbsFile, err)
+					}
+				}
+			} else {
+				log.Errorf("Failed to read remote cluster secret directory [%s]: %v", secretAbsDir, err)
+			}
 		}
-		return []Cluster{}, err
+	} else if !stderrors.Is(err, os.ErrNotExist) {
+		log.Errorf("Failed to read remote cluster secrets directory [%s]: %v", remoteClusterSecretsDir, err)
 	}
 
-	if len(secrets) == 0 {
+	if len(remoteClusterSecrets) == 0 {
 		return []Cluster{}, nil
 	}
 
-	clusters := make([]Cluster, 0, len(secrets))
+	clusters := make([]Cluster, 0, len(remoteClusterSecrets))
 
 	// Inspect the secret to extract the cluster_id and api_endpoint of each remote cluster.
-	for _, secret := range secrets {
-		clusterName, ok := secret.Annotations["networking.istio.io/cluster"]
-		if !ok {
-			clusterName = DefaultClusterID
-		}
-
-		kubeconfigFile, ok := secret.Data[clusterName]
-		if !ok {
-			// We are assuming that the cluster name annotation is also indicating which
-			// key of the secret should contain the kubeconfig file to access the remote cluster.
-			// If there is no such key in the secret, ignore this secret.
-			continue
-		}
+	for clusterName, kubeconfigFile := range remoteClusterSecrets {
 
 		parsedSecret, parseErr := kubernetes.ParseRemoteSecretBytes(kubeconfigFile)
 		if parseErr != nil {
+			log.Errorf("Failed to parse remote cluster secret bytes for cluster [%s]: %v", clusterName, parseErr)
 			continue
 		}
 
 		if len(parsedSecret.Clusters) != 1 {
+			log.Errorf("Parsed remote cluster secret bytes has [%v] clusters", len(parsedSecret.Clusters))
 			continue
 		}
 
 		meshCluster := Cluster{
 			Name:        clusterName,
-			SecretName:  secret.Name,
 			ApiEndpoint: parsedSecret.Clusters[0].Cluster.Server,
 		}
 
diff --git a/business/proxy_logging.go b/business/proxy_logging.go
index 9400cf460..c5e0a97f0 100644
--- a/business/proxy_logging.go
+++ b/business/proxy_logging.go
@@ -1,15 +1,11 @@
 package business
 
 import (
-	"fmt"
-
 	"github.com/kiali/kiali/kubernetes"
 )
 
-var (
-	// ValidProxyLogLevels are the application log levels supported by the envoy admin interface.
-	ValidProxyLogLevels = []string{"off", "trace", "debug", "info", "warning", "error", "critical"}
-)
+// ValidProxyLogLevels are the application log levels supported by the envoy admin interface.
+var ValidProxyLogLevels = []string{"off", "trace", "debug", "info", "warning", "error", "critical"}
 
 // IsValidLogLevel determines if the provided string is a valid proxy log level.
 // This can be called before calling SetLogLevel.
@@ -30,8 +26,9 @@ type ProxyLoggingService struct {
 
 // SetLogLevel sets the pod's proxy log level.
 func (in *ProxyLoggingService) SetLogLevel(namespace, pod, level string) error {
-	if _, err := in.proxyStatus.GetPodProxyStatus(namespace, pod); err != nil {
-		return fmt.Errorf("unable to detect proxy for Pod: %s in Namespace: %s", pod, namespace)
+	// Ensure pod exists
+	if _, err := in.k8s.GetPod(namespace, pod); err != nil {
+		return err
 	}
 	return in.k8s.SetProxyLogLevel(namespace, pod, level)
 }
diff --git a/business/proxy_status.go b/business/proxy_status.go
index 9866ac5a7..e2aeb10b7 100644
--- a/business/proxy_status.go
+++ b/business/proxy_status.go
@@ -3,8 +3,6 @@ package business
 import (
 	"context"
 
-	"k8s.io/client-go/tools/clientcmd/api"
-
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/models"
 )
@@ -14,45 +12,12 @@ type ProxyStatusService struct {
 	businessLayer *Layer
 }
 
-func (in *ProxyStatusService) GetPodProxyStatus(ns, pod string) (*kubernetes.ProxyStatus, error) {
+func (in *ProxyStatusService) GetPodProxyStatus(ns, pod string) *models.ProxyStatus {
 	if kialiCache == nil {
-		return nil, nil
-	}
-
-	if kialiCache.CheckProxyStatus() {
-		return kialiCache.GetPodProxyStatus(ns, pod), nil
-	}
-
-	var proxyStatus []*kubernetes.ProxyStatus
-	var err error
-
-	if proxyStatus, err = in.k8s.GetProxyStatus(); err != nil {
-		if proxyStatus, err = in.getProxyStatusUsingKialiSA(); err != nil {
-			return nil, err
-		}
-	}
-
-	kialiCache.SetProxyStatus(proxyStatus)
-	return kialiCache.GetPodProxyStatus(ns, pod), nil
-}
-
-func (in *ProxyStatusService) getProxyStatusUsingKialiSA() ([]*kubernetes.ProxyStatus, error) {
-	clientFactory, err := kubernetes.GetClientFactory()
-	if err != nil {
-		return nil, err
-	}
-
-	kialiToken, err := kubernetes.GetKialiToken()
-	if err != nil {
-		return nil, err
-	}
-
-	k8s, err := clientFactory.GetClient(&api.AuthInfo{Token: kialiToken})
-	if err != nil {
-		return nil, err
+		return nil
 	}
 
-	return k8s.GetProxyStatus()
+	return castProxyStatus(kialiCache.GetPodProxyStatus(ns, pod))
 }
 
 func castProxyStatus(ps *kubernetes.ProxyStatus) *models.ProxyStatus {
diff --git a/business/references/k8s_gateway_references.go b/business/references/k8s_gateway_references.go
new file mode 100644
index 000000000..bbb83e348
--- /dev/null
+++ b/business/references/k8s_gateway_references.go
@@ -0,0 +1,43 @@
+package references
+
+import (
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/models"
+)
+
+type K8sGatewayReferences struct {
+	K8sGateways   []*k8s_networking_v1beta1.Gateway
+	K8sHTTPRoutes []*k8s_networking_v1beta1.HTTPRoute
+}
+
+func (g K8sGatewayReferences) References() models.IstioReferencesMap {
+	result := models.IstioReferencesMap{}
+
+	for _, gw := range g.K8sGateways {
+		key := models.IstioReferenceKey{Namespace: gw.Namespace, Name: gw.Name, ObjectType: models.ObjectTypeSingular[kubernetes.K8sGateways]}
+		references := &models.IstioReferences{}
+		references.ObjectReferences = g.getConfigReferences(gw)
+		result.MergeReferencesMap(models.IstioReferencesMap{key: references})
+	}
+
+	return result
+}
+
+func (g K8sGatewayReferences) getConfigReferences(gw *k8s_networking_v1beta1.Gateway) []models.IstioReference {
+	result := make([]models.IstioReference, 0)
+
+	for _, rt := range g.K8sHTTPRoutes {
+		if len(rt.Spec.ParentRefs) > 0 {
+			for _, pr := range rt.Spec.ParentRefs {
+				if string(pr.Name) == gw.Name && string(*pr.Kind) == kubernetes.K8sActualGatewayType && string(*pr.Group) == kubernetes.K8sNetworkingGroupVersionV1Beta1.Group {
+					ref := models.IstioReference{Name: rt.Name, Namespace: rt.Namespace, ObjectType: models.ObjectTypeSingular[kubernetes.K8sHTTPRoutes]}
+					result = append(result, ref)
+				}
+			}
+		}
+	}
+
+	return result
+}
diff --git a/business/references/k8shttproute_references.go b/business/references/k8shttproute_references.go
new file mode 100644
index 000000000..8d65b85e9
--- /dev/null
+++ b/business/references/k8shttproute_references.go
@@ -0,0 +1,95 @@
+package references
+
+import (
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/models"
+)
+
+type K8sHTTPRouteReferences struct {
+	Namespaces    models.Namespaces
+	K8sHTTPRoutes []*k8s_networking_v1beta1.HTTPRoute
+}
+
+func (n K8sHTTPRouteReferences) References() models.IstioReferencesMap {
+	result := models.IstioReferencesMap{}
+
+	for _, rt := range n.K8sHTTPRoutes {
+		key := models.IstioReferenceKey{Namespace: rt.Namespace, Name: rt.Name, ObjectType: models.ObjectTypeSingular[kubernetes.K8sHTTPRoutes]}
+		references := &models.IstioReferences{}
+		references.ServiceReferences = n.getServiceReferences(rt)
+		references.ObjectReferences = n.getConfigReferences(rt)
+		result.MergeReferencesMap(models.IstioReferencesMap{key: references})
+	}
+
+	return result
+}
+
+func (n K8sHTTPRouteReferences) getServiceReferences(rt *k8s_networking_v1beta1.HTTPRoute) []models.ServiceReference {
+	keys := make(map[string]bool)
+	allServices := make([]models.ServiceReference, 0)
+	result := make([]models.ServiceReference, 0)
+
+	for _, httpRoute := range rt.Spec.Rules {
+		for _, ref := range httpRoute.BackendRefs {
+			if ref.Kind == nil || string(*ref.Kind) != "Service" {
+				continue
+			}
+			namespace := rt.Namespace
+			if ref.Namespace != nil && string(*ref.Namespace) != "" {
+				namespace = string(*ref.Namespace)
+			}
+			fqdn := kubernetes.GetHost(string(ref.Name), namespace, n.Namespaces.GetNames())
+			if !fqdn.IsWildcard() {
+				allServices = append(allServices, models.ServiceReference{Name: fqdn.Service, Namespace: fqdn.Namespace})
+			}
+		}
+	}
+
+	// filter unique references
+	for _, sv := range allServices {
+		if !keys[sv.Name+"."+sv.Namespace] {
+			result = append(result, sv)
+			keys[sv.Name+"."+sv.Namespace] = true
+		}
+	}
+	return result
+}
+
+func (n K8sHTTPRouteReferences) getConfigReferences(rt *k8s_networking_v1beta1.HTTPRoute) []models.IstioReference {
+	keys := make(map[string]bool)
+	result := make([]models.IstioReference, 0)
+	allGateways := getAllK8sGateways(rt)
+	// filter unique references
+	for _, gw := range allGateways {
+		if !keys[gw.Name+"."+gw.Namespace+"/"+gw.ObjectType] {
+			result = append(result, gw)
+			keys[gw.Name+"."+gw.Namespace+"/"+gw.ObjectType] = true
+		}
+	}
+	return result
+}
+
+func getAllK8sGateways(rt *k8s_networking_v1beta1.HTTPRoute) []models.IstioReference {
+	allGateways := make([]models.IstioReference, 0)
+
+	if len(rt.Spec.ParentRefs) > 0 {
+		for _, parentRef := range rt.Spec.ParentRefs {
+			if string(parentRef.Name) != "" && string(*parentRef.Kind) == kubernetes.K8sActualGatewayType && string(*parentRef.Group) == kubernetes.K8sNetworkingGroupVersionV1Beta1.Group {
+				namespace := rt.Namespace
+				if parentRef.Namespace != nil && string(*parentRef.Namespace) != "" {
+					namespace = string(*parentRef.Namespace)
+				}
+				allGateways = append(allGateways, getK8sGatewayReference(string(parentRef.Name), namespace))
+			}
+		}
+	}
+
+	return allGateways
+}
+
+func getK8sGatewayReference(gateway string, namespace string) models.IstioReference {
+	gw := kubernetes.ParseGatewayAsHost(gateway, namespace)
+	return models.IstioReference{Name: gw.Service, Namespace: gw.Namespace, ObjectType: models.ObjectTypeSingular[kubernetes.K8sGateways]}
+}
diff --git a/business/services.go b/business/services.go
index 4466577e9..4ee53b5ea 100644
--- a/business/services.go
+++ b/business/services.go
@@ -63,6 +63,9 @@ func (in *SvcService) GetServiceList(ctx context.Context, criteria ServiceCriter
 	if criteria.IncludeIstioResources {
 		nFetches = 5
 	}
+	if !config.Get().ExternalServices.Istio.IstioAPIEnabled {
+		nFetches--
+	}
 
 	wg := sync.WaitGroup{}
 	wg.Add(nFetches)
@@ -92,19 +95,21 @@ func (in *SvcService) GetServiceList(ctx context.Context, criteria ServiceCriter
 		}
 	}()
 
-	go func() {
-		defer wg.Done()
-		var err2 error
-		registryCriteria := RegistryCriteria{
-			Namespace:       criteria.Namespace,
-			ServiceSelector: criteria.ServiceSelector,
-		}
-		rSvcs, err2 = in.businessLayer.RegistryStatus.GetRegistryServices(registryCriteria)
-		if err2 != nil {
-			log.Errorf("Error fetching Registry Services per namespace %s: %s", criteria.Namespace, err2)
-			errChan <- err2
-		}
-	}()
+	if config.Get().ExternalServices.Istio.IstioAPIEnabled {
+		go func() {
+			defer wg.Done()
+			var err2 error
+			registryCriteria := RegistryCriteria{
+				Namespace:       criteria.Namespace,
+				ServiceSelector: criteria.ServiceSelector,
+			}
+			rSvcs, err2 = in.businessLayer.RegistryStatus.GetRegistryServices(registryCriteria)
+			if err2 != nil {
+				log.Errorf("Error fetching Registry Services per namespace %s: %s", criteria.Namespace, err2)
+				errChan <- err2
+			}
+		}()
+	}
 
 	go func() {
 		defer wg.Done()
@@ -234,7 +239,7 @@ func (in *SvcService) buildKubernetesServices(svcs []core_v1.Service, pods []cor
 		mPods := models.Pods{}
 		mPods.Parse(sPods)
 		hasSidecar := mPods.HasAnyIstioSidecar()
-		svcVirtualServices := kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, item.Namespace, item.Name)
+		svcVirtualServices := kubernetes.FilterAutogeneratedVirtualServices(kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, item.Namespace, item.Name))
 		svcDestinationRules := kubernetes.FilterDestinationRulesByService(istioConfigList.DestinationRules, item.Namespace, item.Name)
 		svcGateways := kubernetes.FilterGatewaysByVirtualServices(istioConfigList.Gateways, svcVirtualServices)
 		svcK8sHTTPRoutes := kubernetes.FilterK8sHTTPRoutesByService(istioConfigList.K8sHTTPRoutes, item.Namespace, item.Name)
@@ -437,8 +442,12 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, namespace, service,
 	var nsmtls models.MTLSStatus
 
 	wg := sync.WaitGroup{}
-	wg.Add(6)
-	errChan := make(chan error, 6)
+	nwg := 6
+	if !config.Get().ExternalServices.Istio.IstioAPIEnabled {
+		nwg = 4
+	}
+	wg.Add(nwg)
+	errChan := make(chan error, nwg)
 
 	labelsSelector := labels.Set(svc.Selectors).String()
 	// If service doesn't have any selector, we can't know which are the pods and workloads applying.
@@ -470,34 +479,38 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, namespace, service,
 			}
 		}(ctx)
 
+		if config.Get().ExternalServices.Istio.IstioAPIEnabled {
+			go func() {
+				defer wg.Done()
+				var err2 error
+				registryCriteria := RegistryCriteria{
+					Namespace: namespace,
+				}
+				rSvcs, err2 = in.businessLayer.RegistryStatus.GetRegistryServices(registryCriteria)
+				if err2 != nil {
+					log.Errorf("Error fetching Registry Services per namespace %s: %s", registryCriteria.Namespace, err2)
+					errChan <- err2
+				}
+			}()
+		}
+	}
+
+	if config.Get().ExternalServices.Istio.IstioAPIEnabled {
 		go func() {
 			defer wg.Done()
 			var err2 error
-			registryCriteria := RegistryCriteria{
-				Namespace: namespace,
+			criteria := RegistryCriteria{
+				Namespace:   namespace,
+				ServiceName: service,
 			}
-			rSvcs, err2 = in.businessLayer.RegistryStatus.GetRegistryServices(registryCriteria)
+			rEps, err2 = in.businessLayer.RegistryStatus.GetRegistryEndpoints(criteria)
 			if err2 != nil {
-				log.Errorf("Error fetching Registry Services per namespace %s: %s", registryCriteria.Namespace, err2)
+				log.Errorf("Error fetching Registry Endpoints namespace %s and service %s: %s", namespace, service, err2)
 				errChan <- err2
 			}
 		}()
 	}
 
-	go func() {
-		defer wg.Done()
-		var err2 error
-		criteria := RegistryCriteria{
-			Namespace:   namespace,
-			ServiceName: service,
-		}
-		rEps, err2 = in.businessLayer.RegistryStatus.GetRegistryEndpoints(criteria)
-		if err2 != nil {
-			log.Errorf("Error fetching Registry Endpoints namespace %s and service %s: %s", namespace, service, err2)
-			errChan <- err2
-		}
-	}()
-
 	go func(ctx context.Context) {
 		defer wg.Done()
 		var err2 error
@@ -626,7 +639,7 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, namespace, service,
 		Update: vsUpdate,
 		Delete: vsDelete,
 	}
-	s.VirtualServices = kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, namespace, service)
+	s.VirtualServices = kubernetes.FilterAutogeneratedVirtualServices(kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, namespace, service))
 	s.DestinationRules = kubernetes.FilterDestinationRulesByService(istioConfigList.DestinationRules, namespace, service)
 	s.K8sHTTPRoutes = kubernetes.FilterK8sHTTPRoutesByService(istioConfigList.K8sHTTPRoutes, namespace, service)
 	if s.Service.Type == "External" || s.Service.Type == "Federation" {
diff --git a/business/workloads.go b/business/workloads.go
index 8ab9d4099..c531e8f02 100644
--- a/business/workloads.go
+++ b/business/workloads.go
@@ -27,17 +27,33 @@ import (
 	"github.com/kiali/kiali/business/checkers"
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/observability"
 	"github.com/kiali/kiali/prometheus"
 )
 
+func NewWorkloadService(k8s kubernetes.ClientInterface, prom prometheus.ClientInterface, cache cache.KialiCache, layer *Layer, config *config.Config) *WorkloadService {
+	return &WorkloadService{
+		k8s:           k8s,
+		prom:          prom,
+		cache:         cache,
+		businessLayer: layer,
+		config:        config,
+	}
+}
+
 // WorkloadService deals with fetching istio/kubernetes workloads related content and convert to kiali model
 type WorkloadService struct {
-	prom          prometheus.ClientInterface
-	k8s           kubernetes.ClientInterface
+	// Careful not to call the workload service from here as that would be an infinite loop.
 	businessLayer *Layer
+	// The global kiali cache. This should be passed into the workload service rather than created inside of it.
+	cache cache.KialiCache
+	// The global kiali config.
+	config *config.Config
+	k8s    kubernetes.ClientInterface
+	prom   prometheus.ClientInterface
 }
 
 type WorkloadCriteria struct {
@@ -320,7 +336,7 @@ func (in *WorkloadService) GetWorkload(ctx context.Context, criteria WorkloadCri
 	wg.Add(1)
 	go func() {
 		defer wg.Done()
-		conf := config.Get()
+		conf := in.config
 		app := workload.Labels[conf.IstioLabels.AppLabelName]
 		version := workload.Labels[conf.IstioLabels.VersionLabelName]
 		runtimes = NewDashboardsService(ns, workload).GetCustomDashboardRefs(criteria.Namespace, app, version, workload.Pods)
@@ -362,14 +378,17 @@ func (in *WorkloadService) UpdateWorkload(ctx context.Context, namespace string,
 	defer end()
 
 	// Identify controller and apply patch to workload
-	err := updateWorkload(in.businessLayer, namespace, workloadName, workloadType, jsonPatch)
+	err := in.updateWorkload(namespace, workloadName, workloadType, jsonPatch)
 	if err != nil {
 		return nil, err
 	}
 
-	// Cache is stopped after a Create/Update/Delete operation to force a refresh
-	if kialiCache != nil && err == nil {
-		kialiCache.Refresh(namespace)
+	// Cache is stopped after a Create/Update/Delete operation to force a refresh.
+	// Refresh once after all the updates have gone through since Update Workload will update
+	// every single workload type of that matches name/namespace and we only want to refresh once.
+	// TODO: Remove conditional once cache is mandatory
+	if in.cache != nil {
+		in.cache.Refresh(namespace)
 	}
 
 	// After the update we fetch the whole workload
@@ -385,21 +404,17 @@ func (in *WorkloadService) GetPods(ctx context.Context, namespace string, labelS
 	)
 	defer end()
 
-	var err error
-	var ps []core_v1.Pod
-	// Check if namespace is cached
-	if IsNamespaceCached(namespace) {
-		// Cache uses Kiali ServiceAccount, check if user can access to the namespace
-		if _, err = in.businessLayer.Namespace.GetNamespace(ctx, namespace); err == nil {
-			ps, err = kialiCache.GetPods(namespace, labelSelector)
-		}
-	} else {
-		ps, err = in.k8s.GetPods(namespace, labelSelector)
+	// Check if user has access to the namespace
+	if _, err := in.businessLayer.Namespace.GetNamespace(ctx, namespace); err != nil {
+		return nil, err
 	}
 
-	if err != nil {
+	var ps []core_v1.Pod
+	var err error
+	if ps, err = in.k8s.GetPods(namespace, labelSelector); err != nil {
 		return nil, err
 	}
+
 	pods := models.Pods{}
 	pods.Parse(ps)
 	return pods, nil
@@ -426,7 +441,7 @@ func (in *WorkloadService) BuildLogOptionsCriteria(container, duration, isProxy,
 	if duration != "" {
 		duration, err := time.ParseDuration(duration)
 		if err != nil {
-			return nil, fmt.Errorf("Invalid duration [%s]: %v", duration, err)
+			return nil, fmt.Errorf("invalid duration [%s]: %v", duration, err)
 		}
 
 		opts.Duration = &duration
@@ -437,7 +452,7 @@ func (in *WorkloadService) BuildLogOptionsCriteria(container, duration, isProxy,
 	if sinceTime != "" {
 		numTime, err := strconv.ParseInt(sinceTime, 10, 64)
 		if err != nil {
-			return nil, fmt.Errorf("Invalid sinceTime [%s]: %v", sinceTime, err)
+			return nil, fmt.Errorf("invalid sinceTime [%s]: %v", sinceTime, err)
 		}
 
 		opts.SinceTime = &meta_v1.Time{Time: time.Unix(numTime, 0)}
@@ -449,7 +464,7 @@ func (in *WorkloadService) BuildLogOptionsCriteria(container, duration, isProxy,
 				opts.MaxLines = &numLines
 			}
 		} else {
-			return nil, fmt.Errorf("Invalid maxLines [%s]: %v", maxLines, err)
+			return nil, fmt.Errorf("invalid maxLines [%s]: %v", maxLines, err)
 		}
 	}
 
@@ -604,15 +619,8 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 	// Pods are always fetched
 	go func() {
 		defer wg.Done()
-
-		// Check if namespace is cached
-		// Namespace access is checked in the upper caller
 		var err error
-		if IsNamespaceCached(namespace) {
-			pods, err = kialiCache.GetPods(namespace, labelSelector)
-		} else {
-			pods, err = layer.k8s.GetPods(namespace, labelSelector)
-		}
+		pods, err = layer.Workload.k8s.GetPods(namespace, labelSelector)
 		if err != nil {
 			log.Errorf("Error fetching Pods per namespace %s: %s", namespace, err)
 			errChan <- err
@@ -622,15 +630,8 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 	// Deployments are always fetched
 	go func() {
 		defer wg.Done()
-
-		// Check if namespace is cached
-		// Namespace access is checked in the upper caller
 		var err error
-		if IsNamespaceCached(namespace) {
-			dep, err = kialiCache.GetDeployments(namespace)
-		} else {
-			dep, err = layer.k8s.GetDeployments(namespace)
-		}
+		dep, err = layer.Workload.k8s.GetDeployments(namespace)
 		if err != nil {
 			log.Errorf("Error fetching Deployments per namespace %s: %s", namespace, err)
 			errChan <- err
@@ -640,15 +641,8 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 	// ReplicaSets are always fetched
 	go func() {
 		defer wg.Done()
-
-		// Check if namespace is cached
-		// Namespace access is checked in the upper caller
 		var err error
-		if IsNamespaceCached(namespace) {
-			repset, err = kialiCache.GetReplicaSets(namespace)
-		} else {
-			repset, err = layer.k8s.GetReplicaSets(namespace)
-		}
+		repset, err = layer.Workload.k8s.GetReplicaSets(namespace)
 		if err != nil {
 			log.Errorf("Error fetching ReplicaSets per namespace %s: %s", namespace, err)
 			errChan <- err
@@ -661,7 +655,7 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 
 		var err error
 		if isWorkloadIncluded(kubernetes.ReplicationControllerType) {
-			repcon, err = layer.k8s.GetReplicationControllers(namespace)
+			repcon, err = layer.Workload.k8s.GetReplicationControllers(namespace)
 			if err != nil {
 				log.Errorf("Error fetching GetReplicationControllers per namespace %s: %s", namespace, err)
 				errChan <- err
@@ -674,8 +668,8 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 		defer wg.Done()
 
 		var err error
-		if layer.k8s.IsOpenShift() && isWorkloadIncluded(kubernetes.DeploymentConfigType) {
-			depcon, err = layer.k8s.GetDeploymentConfigs(namespace)
+		if layer.Workload.k8s.IsOpenShift() && isWorkloadIncluded(kubernetes.DeploymentConfigType) {
+			depcon, err = layer.Workload.k8s.GetDeploymentConfigs(namespace)
 			if err != nil {
 				log.Errorf("Error fetching DeploymentConfigs per namespace %s: %s", namespace, err)
 				errChan <- err
@@ -689,11 +683,7 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 
 		var err error
 		if isWorkloadIncluded(kubernetes.StatefulSetType) {
-			if IsNamespaceCached(namespace) {
-				fulset, err = kialiCache.GetStatefulSets(namespace)
-			} else {
-				fulset, err = layer.k8s.GetStatefulSets(namespace)
-			}
+			fulset, err = layer.Workload.k8s.GetStatefulSets(namespace)
 			if err != nil {
 				log.Errorf("Error fetching StatefulSets per namespace %s: %s", namespace, err)
 				errChan <- err
@@ -707,7 +697,7 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 
 		var err error
 		if isWorkloadIncluded(kubernetes.CronJobType) {
-			conjbs, err = layer.k8s.GetCronJobs(namespace)
+			conjbs, err = layer.Workload.k8s.GetCronJobs(namespace)
 			if err != nil {
 				log.Errorf("Error fetching CronJobs per namespace %s: %s", namespace, err)
 				errChan <- err
@@ -721,7 +711,7 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 
 		var err error
 		if isWorkloadIncluded(kubernetes.JobType) {
-			jbs, err = layer.k8s.GetJobs(namespace)
+			jbs, err = layer.Workload.k8s.GetJobs(namespace)
 			if err != nil {
 				log.Errorf("Error fetching Jobs per namespace %s: %s", namespace, err)
 				errChan <- err
@@ -735,11 +725,7 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 
 		var err error
 		if isWorkloadIncluded(kubernetes.DaemonSetType) {
-			if IsNamespaceCached(namespace) {
-				daeset, err = kialiCache.GetDaemonSets(namespace)
-			} else {
-				daeset, err = layer.k8s.GetDaemonSets(namespace)
-			}
+			daeset, err = layer.Workload.k8s.GetDaemonSets(namespace)
 			if err != nil {
 				log.Errorf("Error fetching DaemonSets per namespace %s: %s", namespace, err)
 			}
@@ -1132,12 +1118,8 @@ func fetchWorkloads(ctx context.Context, layer *Layer, namespace string, labelSe
 
 		// Add the Proxy Status to the workload
 		for _, pod := range w.Pods {
-			if pod.HasIstioSidecar() {
-				ps, err := layer.ProxyStatus.GetPodProxyStatus(namespace, pod.Name)
-				if err != nil {
-					log.Warningf("GetPodProxyStatus is failing for [namespace: %s] [pod: %s]: %s ", namespace, pod.Name, err.Error())
-				}
-				pod.ProxyStatus = castProxyStatus(ps)
+			if pod.HasIstioSidecar() && config.Get().ExternalServices.Istio.IstioAPIEnabled {
+				pod.ProxyStatus = layer.ProxyStatus.GetPodProxyStatus(namespace, pod.Name)
 			}
 		}
 
@@ -1185,15 +1167,8 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 	// Pods are always fetched for all workload types
 	go func() {
 		defer wg.Done()
-
-		// Check if namespace is cached
-		// Namespace access is checked in the upper call
 		var err error
-		if IsNamespaceCached(criteria.Namespace) {
-			pods, err = kialiCache.GetPods(criteria.Namespace, "")
-		} else {
-			pods, err = layer.k8s.GetPods(criteria.Namespace, "")
-		}
+		pods, err = layer.Workload.k8s.GetPods(criteria.Namespace, "")
 		if err != nil {
 			log.Errorf("Error fetching Pods per namespace %s: %s", criteria.Namespace, err)
 			errChan <- err
@@ -1203,19 +1178,12 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 	// fetch as Deployment when workloadType is Deployment or unspecified
 	go func() {
 		defer wg.Done()
+		var err error
 
 		if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.DeploymentType {
 			return
 		}
-
-		// Check if namespace is cached
-		// Namespace access is checked in the upper call
-		var err error
-		if IsNamespaceCached(criteria.Namespace) {
-			dep, err = kialiCache.GetDeployment(criteria.Namespace, criteria.WorkloadName)
-		} else {
-			dep, err = layer.k8s.GetDeployment(criteria.Namespace, criteria.WorkloadName)
-		}
+		dep, err = layer.Workload.k8s.GetDeployment(criteria.Namespace, criteria.WorkloadName)
 		if err != nil {
 			if errors.IsNotFound(err) {
 				dep = nil
@@ -1233,15 +1201,8 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 		if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.ReplicaSetType && knownWorkloadType {
 			return
 		}
-
-		// Check if namespace is cached
-		// Namespace access is checked in the upper call
 		var err error
-		if IsNamespaceCached(criteria.Namespace) {
-			repset, err = kialiCache.GetReplicaSets(criteria.Namespace)
-		} else {
-			repset, err = layer.k8s.GetReplicaSets(criteria.Namespace)
-		}
+		repset, err = layer.Workload.k8s.GetReplicaSets(criteria.Namespace)
 		if err != nil {
 			log.Errorf("Error fetching ReplicaSets per namespace %s: %s", criteria.Namespace, err)
 			errChan <- err
@@ -1258,7 +1219,7 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 
 		var err error
 		if isWorkloadIncluded(kubernetes.ReplicationControllerType) {
-			repcon, err = layer.k8s.GetReplicationControllers(criteria.Namespace)
+			repcon, err = layer.Workload.k8s.GetReplicationControllers(criteria.Namespace)
 			if err != nil {
 				log.Errorf("Error fetching GetReplicationControllers per namespace %s: %s", criteria.Namespace, err)
 				errChan <- err
@@ -1275,8 +1236,8 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 		}
 
 		var err error
-		if layer.k8s.IsOpenShift() && isWorkloadIncluded(kubernetes.DeploymentConfigType) {
-			depcon, err = layer.k8s.GetDeploymentConfig(criteria.Namespace, criteria.WorkloadName)
+		if layer.Workload.k8s.IsOpenShift() && isWorkloadIncluded(kubernetes.DeploymentConfigType) {
+			depcon, err = layer.Workload.k8s.GetDeploymentConfig(criteria.Namespace, criteria.WorkloadName)
 			if err != nil {
 				depcon = nil
 			}
@@ -1293,11 +1254,7 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 
 		var err error
 		if isWorkloadIncluded(kubernetes.StatefulSetType) {
-			if IsNamespaceCached(criteria.Namespace) {
-				fulset, err = kialiCache.GetStatefulSet(criteria.Namespace, criteria.WorkloadName)
-			} else {
-				fulset, err = layer.k8s.GetStatefulSet(criteria.Namespace, criteria.WorkloadName)
-			}
+			fulset, err = layer.Workload.k8s.GetStatefulSet(criteria.Namespace, criteria.WorkloadName)
 			if err != nil {
 				fulset = nil
 			}
@@ -1314,7 +1271,7 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 
 		var err error
 		if isWorkloadIncluded(kubernetes.CronJobType) {
-			conjbs, err = layer.k8s.GetCronJobs(criteria.Namespace)
+			conjbs, err = layer.Workload.k8s.GetCronJobs(criteria.Namespace)
 			if err != nil {
 				log.Errorf("Error fetching CronJobs per namespace %s: %s", criteria.Namespace, err)
 				errChan <- err
@@ -1332,7 +1289,7 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 
 		var err error
 		if isWorkloadIncluded(kubernetes.JobType) {
-			jbs, err = layer.k8s.GetJobs(criteria.Namespace)
+			jbs, err = layer.Workload.k8s.GetJobs(criteria.Namespace)
 			if err != nil {
 				log.Errorf("Error fetching Jobs per namespace %s: %s", criteria.Namespace, err)
 				errChan <- err
@@ -1350,11 +1307,7 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 
 		var err error
 		if isWorkloadIncluded(kubernetes.DaemonSetType) {
-			if IsNamespaceCached(criteria.Namespace) {
-				ds, err = kialiCache.GetDaemonSet(criteria.Namespace, criteria.WorkloadName)
-			} else {
-				ds, err = layer.k8s.GetDaemonSet(criteria.Namespace, criteria.WorkloadName)
-			}
+			ds, err = layer.Workload.k8s.GetDaemonSet(criteria.Namespace, criteria.WorkloadName)
 			if err != nil {
 				ds = nil
 			}
@@ -1718,11 +1671,9 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 		// Add the Proxy Status to the workload
 		for _, pod := range w.Pods {
 			if pod.HasIstioSidecar() {
-				ps, err := layer.ProxyStatus.GetPodProxyStatus(criteria.Namespace, pod.Name)
-				if err != nil {
-					log.Warningf("GetPodProxyStatus is failing for [namespace: %s] [pod: %s]: %s ", criteria.Namespace, pod.Name, err.Error())
+				if config.Get().ExternalServices.Istio.IstioAPIEnabled {
+					pod.ProxyStatus = layer.ProxyStatus.GetPodProxyStatus(criteria.Namespace, pod.Name)
 				}
-				pod.ProxyStatus = castProxyStatus(ps)
 			}
 		}
 
@@ -1733,10 +1684,10 @@ func fetchWorkload(ctx context.Context, layer *Layer, criteria WorkloadCriteria)
 	return wl, kubernetes.NewNotFound(criteria.WorkloadName, "Kiali", "Workload")
 }
 
-func updateWorkload(layer *Layer, namespace string, workloadName string, workloadType string, jsonPatch string) error {
+func (in *WorkloadService) updateWorkload(namespace string, workloadName string, workloadType string, jsonPatch string) error {
 	// Check if user has access to the namespace (RBAC) in cache scenarios and/or
 	// if namespace is accessible from Kiali (Deployment.AccessibleNamespaces)
-	if _, err := layer.Namespace.GetNamespace(context.TODO(), namespace); err != nil {
+	if _, err := in.businessLayer.Namespace.GetNamespace(context.TODO(), namespace); err != nil {
 		return err
 	}
 
@@ -1776,7 +1727,7 @@ func updateWorkload(layer *Layer, namespace string, workloadName string, workloa
 			defer wg.Done()
 			var err error
 			if isWorkloadIncluded(wkType) {
-				err = layer.k8s.UpdateWorkload(namespace, workloadName, wkType, jsonPatch)
+				err = in.k8s.UpdateWorkload(namespace, workloadName, wkType, jsonPatch)
 			}
 			if err != nil {
 				if !errors.IsNotFound(err) {
@@ -1844,7 +1795,7 @@ func (in *WorkloadService) GetWorkloadAppName(ctx context.Context, namespace, wo
 		return "", err
 	}
 
-	appLabelName := config.Get().IstioLabels.AppLabelName
+	appLabelName := in.config.IstioLabels.AppLabelName
 	app := wkd.Labels[appLabelName]
 	return app, nil
 }
diff --git a/config/config.go b/config/config.go
index 84bf47a1c..f57526820 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,10 +1,8 @@
 package config
 
 import (
-	"embed"
 	"errors"
 	"fmt"
-	"io/fs"
 	"os"
 	"sort"
 	"strings"
@@ -196,6 +194,7 @@ type TracingConfig struct {
 	IsCore               bool              `yaml:"is_core,omitempty"`
 	NamespaceSelector    bool              `yaml:"namespace_selector"`
 	QueryScope           map[string]string `yaml:"query_scope,omitempty"`
+	QueryTimeout         int               `yaml:"query_timeout,omitempty"`
 	URL                  string            `yaml:"url"`
 	UseGRPC              bool              `yaml:"use_grpc"`
 	WhiteListIstioSystem []string          `yaml:"whitelist_istio_system"`
@@ -214,6 +213,7 @@ type IstioConfig struct {
 	ConfigMapName                     string              `yaml:"config_map_name,omitempty"`
 	EnvoyAdminLocalPort               int                 `yaml:"envoy_admin_local_port,omitempty"`
 	GatewayAPIClassName               string              `yaml:"gateway_api_class_name,omitempty"`
+	IstioAPIEnabled                   bool                `yaml:"istio_api_enabled,omitempty"`
 	IstioCanaryRevision               IstioCanaryRevision `yaml:"istio_canary_revision,omitempty"`
 	IstioIdentityDomain               string              `yaml:"istio_identity_domain,omitempty"`
 	IstioInjectionAnnotation          string              `yaml:"istio_injection_annotation,omitempty"`
@@ -284,7 +284,8 @@ type KubernetesConfig struct {
 	// Cache uses watchers to sync with the backend, after a CacheDuration watchers are closed and re-opened
 	CacheDuration int `yaml:"cache_duration,omitempty"`
 	// Enable cache for kubernetes and istio resources
-	CacheEnabled bool `yaml:"cache_enabled,omitempty"`
+	// TODO: Remove once all services are migrated to use the cache.
+	CacheEnabled bool `yaml:"-,omitempty"`
 	// Kiali can cache VirtualService,DestinationRule,Gateway and ServiceEntry Istio resources if they are present
 	// on this list of Istio types. Other Istio types are not yet supported.
 	CacheIstioTypes []string `yaml:"cache_istio_types,omitempty"`
@@ -458,39 +459,6 @@ type HealthConfig struct {
 	Rate []Rate `yaml:"rate,omitempty" json:"rate,omitempty"`
 }
 
-//go:embed *
-var compatibilityMatrixFile embed.FS
-
-// CompatibilityMatric version ranges for compatibility details between Istio and Kiali
-type CompatibilityMatrix []struct {
-	MeshName     string `yaml:"meshName"`
-	VersionRange []struct {
-		MeshVersion         string   `yaml:"meshVersion"`
-		KialiMinimumVersion string   `yaml:"kialiMinimumVersion,omitempty"`
-		KialiMaximumVersion string   `yaml:"kialiMaximumVersion,omitempty"`
-		KialiFixedVersion   []string `yaml:"kialiFixedVersion,omitempty"`
-	} `yaml:"versionRange"`
-}
-
-// NewCompatibilityMatrix return compatible kiali versions for mesh
-func NewCompatibilityMatrix() (CompatibilityMatrix, error) {
-	in, err := fs.ReadFile(compatibilityMatrixFile, "version-compatibility-matrix.yaml")
-	if err != nil {
-		log.Warningf("Can not load compatibility matrix file. Error: %v", err)
-		return nil, err
-	}
-
-	var matrix CompatibilityMatrix
-
-	err = yaml.Unmarshal(in, &matrix)
-	if err != nil {
-		log.Warningf("Can not unmarshal compatibility matrix file. Error: %v", err)
-		return nil, err
-	}
-
-	return matrix, nil
-}
-
 // Config defines full YAML configuration.
 type Config struct {
 	AdditionalDisplayDetails []AdditionalDisplayItem             `yaml:"additional_display_details,omitempty"`
@@ -604,6 +572,7 @@ func NewConfig() (c *Config) {
 				},
 				ConfigMapName:                     "istio",
 				EnvoyAdminLocalPort:               15000,
+				IstioAPIEnabled:                   true,
 				IstioIdentityDomain:               "svc.cluster.local",
 				IstioInjectionAnnotation:          "sidecar.istio.io/inject",
 				IstioSidecarInjectorConfigMapName: "istio-sidecar-injector",
@@ -641,6 +610,7 @@ func NewConfig() (c *Config) {
 				IsCore:               false,
 				NamespaceSelector:    true,
 				QueryScope:           map[string]string{},
+				QueryTimeout:         5,
 				URL:                  "",
 				UseGRPC:              true,
 				WhiteListIstioSystem: []string{"jaeger-query", "istio-ingressgateway"},
diff --git a/config/version-compatibility-matrix.yaml b/config/version-compatibility-matrix.yaml
deleted file mode 100644
index 54ac38d8c..000000000
--- a/config/version-compatibility-matrix.yaml
+++ /dev/null
@@ -1,73 +0,0 @@
-# The Compatible Version Matrix between upstream Istio and Kiali
-- meshName: "Istio"
-  versionRange:
-    - meshVersion: "1.16"
-      kialiMinimumVersion: "1.59.1"
-      kialiMaximumVersion: ""
-    - meshVersion: "1.15"
-      kialiMinimumVersion: "1.55.0"
-      kialiMaximumVersion: "1.59.0"
-    - meshVersion: "1.14"
-      kialiMinimumVersion: "1.50.0"
-      kialiMaximumVersion: "1.54"
-    - meshVersion: "1.13"
-      kialiMinimumVersion: "1.45.1"
-      kialiMaximumVersion: "1.49"
-    - meshVersion: "1.12"
-      kialiMinimumVersion: "1.42.0"
-      kialiMaximumVersion: "1.44.0"
-    - meshVersion: "1.11"
-      kialiMinimumVersion: "1.38.1"
-      kialiMaximumVersion: "1.41"
-    - meshVersion: "1.10"
-      kialiMinimumVersion: "1.34.1"
-      kialiMaximumVersion: "1.37"
-    - meshVersion: "1.9"
-      kialiMinimumVersion: "1.29.1"
-      kialiMaximumVersion: "1.33"
-    - meshVersion: "1.8"
-      kialiMinimumVersion: "1.26.0"
-      kialiMaximumVersion: "1.28"
-    - meshVersion: "1.7"
-      kialiMinimumVersion: "1.22.1"
-      kialiMaximumVersion: "1.25"
-    - meshVersion: "1.6"
-      kialiMinimumVersion: "1.18.1"
-      kialiMaximumVersion: "1.21"
-    - meshVersion: "1.5"
-      kialiFixedVersion: 
-      - "1.17"
-    - meshVersion: "1.4"
-      kialiFixedVersion: 
-      - "1.17"
-    - meshVersion: "1.3"
-      kialiFixedVersion: 
-      - "1.17"
-    - meshVersion: "1.2"
-      kialiFixedVersion: 
-      - "1.17"
-    - meshVersion: "1.1"
-      kialiFixedVersion: 
-      - "1.17"
-    - meshVersion: "1.0"
-      kialiFixedVersion: 
-      - "1.17"
-
-# The Compatible Version Matrix between OpenShift Service Mesh and Kiali (there is a 1-to-1 relationship between Kiali and OSSM versions)
-- meshName: "OpenShift Service Mesh"
-  versionRange: 
-    - meshVersion: "2.3"
-      kialiFixedVersion:
-      - "1.57"
-    - meshVersion: "2.2"
-      kialiFixedVersion: 
-      - "1.48"
-    - meshVersion: "2.1"
-      kialiFixedVersion: 
-      - "1.36"
-    - meshVersion: "2.0"
-      kialiFixedVersion:
-      - "1.24"
-    - meshVersion: "1.1"
-      kialiFixedVersion: 
-      - "1.12"
diff --git a/design/KEPS/multicluster/Multi-Primary.png b/design/KEPS/multicluster/Multi-Primary.png
new file mode 100644
index 000000000..0c47326c4
Binary files /dev/null and b/design/KEPS/multicluster/Multi-Primary.png differ
diff --git a/design/KEPS/multicluster/Primary-Remote.png b/design/KEPS/multicluster/Primary-Remote.png
new file mode 100644
index 000000000..b33a386ad
Binary files /dev/null and b/design/KEPS/multicluster/Primary-Remote.png differ
diff --git a/design/KEPS/multicluster/proposal.md b/design/KEPS/multicluster/proposal.md
new file mode 100644
index 000000000..a99127d30
--- /dev/null
+++ b/design/KEPS/multicluster/proposal.md
@@ -0,0 +1,169 @@
+# Multicluster support
+
+1. [Summary](#summary)
+2. [Motivation](#motivation)
+   1. [Goals](#goals)
+   2. [Non-Goals](#nongoals)
+3. [Solution](#solution)
+   1. [Other solutions](#othersolutions)
+4. [Roadmap](#roadmap)
+
+# Summary
+
+Multi-cluster support for Kiali. A “single pane of glass” for your istio service mesh.
+
+[github discussions](https://github.com/kiali/kiali/discussions/categories/multicluster)
+
+# Motivation
+
+It is now common for meshes to span across multiple clusters and yet Kiali only partially supports multi-cluster deployments today. Users should be able to visualize their workloads on the graph across clusters, dig into details via the workload/app/service pages, receive validations for their istio config etc. across their whole mesh from a single Kiali.
+
+## Goals
+
+- Multi-cluster mesh support - single pane of glass for your entire mesh.
+- Support multi-primary and primary-remote Istio deployment models.
+- Support RBAC authentication strategy for multi-cluster deployments.
+
+## Non-goals
+
+- Custom aggregation of all Kiali’s datasources across clusters. There are other tools and initiatives for aggregating metrics/logs/traces such as otel and this does not need to be handled by Kiali.
+- Ambient is not going to be touched in this effort.
+- Multi-mesh support - multi-cluster is focused on single mesh support only but shouldn’t prevent Kiali from supporting multi-mesh in the future.
+
+# Solution
+
+A single Kiali configured to access each Kubernetes cluster that is part of the mesh. Kiali will require Metrics/traces to be aggregated in a centralized datastore that Kiali can query.
+
+In a multicluster deployment, Kiali will only support the OIDC auth strategy for now.
+
+## Deployment models
+
+In each of the [deployment models](https://istio.io/latest/docs/ops/deployment/deployment-models/), these are the requirements for a centralized Kiali
+
+1. Access to each kube API server that is part of the mesh
+2. Istiod access\*
+3. Aggregated metrics/traces
+4. OIDC auth strategy
+
+\*Regarding istiod access, if the istiod pods are running within the Kube clusters that are part of the multi-cluster mesh, Kiali will access these by portforwarding using the Kube API. Kiali can support control planes outside of but some features will be lost when Kiali can not access the istiod API.
+
+A service account in each cluster will provide Kiali with access to that cluster's Kube API. Credentials for that service account will be stored in a secret and used by the Kiali pod to communicate with the external Kube APIs.
+
+Note that a single or different network topology won't affect Kiali's configuration and deployment because Kiali will only need access to the Kubernetes API.
+
+### Multi Primary deployment
+
+![Multi-Primary](Multi-Primary.png "Multi Primary")
+
+### Primary-Remote deployment
+
+![Primary-Remote](Primary-Remote.png "Primary Remote")
+
+## Aggregated metrics and traces
+
+Kiali MC will require metrics to be aggregated into a central, unified datastore. It will not aggregate metrics from multiple datastores. The same is true for trace information. It is the user’s responsibility to ensure that metrics and traces are centralized by some means. Metrics and traces will need to include a cluster identifier as an attribute/label on each metric so that Kiali can distinguish between workloads/services running on separate clusters. [Istio standard metrics](https://istio.io/latest/docs/reference/config/metrics/#labels) includes `destination_cluster` and `source_cluster`. Kiali already uses these values for cluster boxing. Traces do not include a cluster identifier but one can be added by creating a telemetry api object. Example:
+
+```
+apiVersion: telemetry.istio.io/v1alpha1
+kind: Telemetry
+metadata:
+  name: mesh-default
+  namespace: istio-system
+spec:
+  tracing:
+  - customTags:
+      cluster:
+        literal:
+          value: Kubernetes
+```
+
+[query_scope](https://kiali.io/docs/configuration/kialis.kiali.io/#.spec.external_services.prometheus.query_scope) can be used to limit what Kiali sees from the prometheus and jaeger queries to include only workloads/services within the mesh if these datastores are shared with non-mesh workloads.
+
+## Authentication schemes
+
+In single-cluster, Kiali currently supports the following [authentication schemes](https://kiali.io/docs/configuration/authentication/):
+
+- Anonymous
+- Header
+- OpenID Connect (OIDC)
+- OpenShift
+- Token
+
+Of these, only OpenID Connect and Header currently provide the ability to perform cross-cluster authentication. The Token scheme is tied to a single cluster since it uses service account tokens tied to service accounts in a single cluster. The openshift strategy does not support multiple clusters.
+
+The security model Kiali has today assumes a namespace boundary scoped to a single cluster. Of the different authentication options that Kiali supports, RBAC would work cross-cluster if RBAC was managed by an external provider that spanned across the clusters. This requires an admin to properly setup the OIDC provider across clusters.
+
+The namespace service is used to determine if a user has access to resources in that namespace: https://github.com/kiali/kiali/blob/master/business/namespaces.go#L64. For multi-cluster, Kiali will check the user's permissions for that namespace across all clusters. As an optimization, the namespace service can accept a list of specific clusters to query against.
+
+## API
+
+Kiali’s current model of a Service/Workload/App (s/w/a) uses namespace/name to uniquely identify a particular s/w/a. For example, a “reviews” service in namespace “bookinfo” is uniquely identified as “reviews/bookinfo”. This model works fine for a single cluster deployment but does not uniquely identify a service in multi-cluster. Services and workloads can span multiple clusters.
+
+With some exceptions, Istio sees two services with the same FQDN across two different clusters as the same service and will load balance traffic between the two. You can see this when verifying your [multi-cluster traffic](https://istio.io/latest/docs/setup/install/multicluster/verify/#verifying-cross-cluster-traffic) with requests being load balanced across services located in both clusters. Therefore in a multi-cluster deployment, Kiali should no longer treat namespace/name as unique and consider services and workloads with the same namespace/name _across_ clusters as the same service or workload. This adheres to [namespace sameness](https://github.com/kubernetes/community/blob/master/sig-multicluster/namespace-sameness-position-statement.md).
+
+Exceptions to this are:
+
+1. [Cluster local services](https://istio.io/latest/docs/ops/configuration/traffic-management/multicluster/#keeping-traffic-in-cluster) configured either locally or globally for a particular service.
+2. [Workloads partitioned](https://istio.io/latest/docs/ops/configuration/traffic-management/multicluster/#partitioning-services) by destination rule / Virtual service.
+
+The API routes to a particular s/w/a based on the namespace/name e.g. `/api/namespaces/{namespace}/workloads/{workload}`. These namespace specific routes will stay the same but they will include results from all clusters. e.g. given a `bookinfo/reviews` service in cluster A and cluster B, `/api/namespaces/bookinfo/services/reviews` would return info about a reviews service from both cluster A and B.
+
+It will be up to the business layer to aggregate all of the info across clusters. Fetching workloads and services will need to be done inside the business layer for every cluster Kiali is connected to. Since details of workloads/services can vary based on the cluster, e.g. the `reviews.bookinfo` Service in cluster A may have different labels than on cluster B and Kiali needs to show those details, the Kiali details models will need to distinguish between the `Service` objects on cluster A vs. cluster B. Istio config will also need a way to uniquely identify istio config across clusters. Istio configuration is not shared across clusters and is specific to the cluster it resides in.
+
+Some services/workloads may span across clusters but some may not. Business layer services can optionally accept a list of specific clusters to query against for a given s/w/a as an optimization. The business services will need to handle this aggregation properly and cannot assume that s/w/a will exist in all clusters or only in a single cluster.
+
+## UI/UX
+
+This proposal focuses mostly on the backend changes necessary to enable a multi-cluster deployment of Kiali however some UI elements can and should change with a multi-cluster deployment. Exact details of UI changes will be left to future issues.
+
+## Configuration
+
+To deploy Kiali MC, users are expected to configure Kiali with access to:
+
+- Unified Prometheus
+- Unified Jaeger
+- 1..N Kube API servers where N is the number of Kube clusters in the mesh. Kiali will require Kube API credentials to communicate with each of these Kube API servers.
+
+Access to the Kube API servers can be granted by:
+
+1. Creating a service account in the target cluster.
+2. Granting proper permissions for this service account.
+3. Creating a secret with the credentials for this service account in the cluster/namespace where Kiali is deployed.
+
+This process will need to be repeated for every new cluster added. Adding or removing clusters will also require restarting the Kiali pod in order for the cache watches to pick up the new kube config. Adding/removing kube clusters without having to restart Kiali can be an improvement made later.
+
+## Operator
+
+The Kiali operator will still be used for configuring Kiali itself however, it's assumed that the operator won't have access to other kube clusters other than where Kiali is deployed and therefore configuring multicluster access for Kiali will be a manual operation. The operator will remain unchanged in a multi-cluster deployment.
+
+In order to configure the Kiali Server for multicluster access, you must manually create remote cluster secrets in the same namepace where the Kiali Server will be deployed (normally this is the control plane namespace, aka `istio-system`, but that is not required). As a convenience, you can create these remote cluster secrets by using `istioctl`. For example, suppose you have all your remote cluster credentials defined in local kube contexts (e.g. contexts named remote1 and remote2). You can create these remote cluster secrets via:
+
+`istioctl x create-remote-secret --name remote-cluster-1-name --context remote1 --namespace istio-system`
+and
+`istioctl x create-remote-secret --name remote-cluster-2-name --context remote2 --namespace istio-system`
+
+By default, the Kiali Operator will autodiscover all remote cluster secrets found in the Kiali deployment namespace and will mount them to the Kiali Server pod at install time. When the Kiali Server starts up, it will obtain all the necessary remote cluster connection information from the mounted secrets.
+
+## Testing
+
+All automated e2e testing, API and Cypress, is currently performed against a single cluster in CI. There will be a large number of Kiali features related to multi-cluster so there needs to be a way to test multi-cluster deployments frequently and in an automated fashion. Any e2e tests that require a multi-cluster environment will need to be run separately from the rest of the suite.
+
+## Other solutions
+
+### Kiali Agent
+
+A process that runs on each kube cluster that is a part of the mesh and exports Kiali’s view of that mesh to a centralized Kiali. This option offers potential performance gains and ease of configuration since Kiali no longer has to communicate with each individual kube cluster. However, it complicates Kiali’s ability to perform authorization with RBAC since the agents would be exporting data to a centralized Kiali and the centralized Kiali would not have access to the individual clusters. The agent itself adds an additional component to Kiali that would need to be maintained and configured across clusters.
+
+### Connecting directly to istiod
+
+The istiods provide istio config and some information about the mesh workloads. However, the endpoints are not always exposed outside of the istiod pod. Kiali today uses port forwarding through the Kubernetes API to connect to the istiod pods and thus requires access to the kube API . Some other features rely on the Kube API like streaming logs and any create/update operations need to use the Kube API ultimately making this an un-viable option to keep most of the existing Kiali features.
+
+# Roadmap
+
+- [ ] Multicluster Kubernetes support for Kiali.
+  - [ ] Cache can be configured for multiple kubernetes clusters.
+  - [ ] Client can be configured for multiple kubernetes clusters.
+- [ ] Business services query across configured clusters for resources.
+- [ ] Investigate automated testing setup: how kiali should test multi-cluster features in CI/CD.
+- [ ] Multi-cluster demos for the different deployment models. These can also be used for testing.
+- [ ] Define UI/UX changes to support MC
diff --git a/frontend/cypress/integration/common/apps.ts b/frontend/cypress/integration/common/apps.ts
index 37109bc5c..db8cff7d6 100644
--- a/frontend/cypress/integration/common/apps.ts
+++ b/frontend/cypress/integration/common/apps.ts
@@ -85,10 +85,14 @@ And('user sees Details information for Apps', () => {
   });
 });
 
-Then('user only sees {int} apps', (sees: number) => {
+And('user only sees the apps with the {string} name in the {string} namespace', (name:string,ns:string) => {
+  let count:number;
+  cy.request('GET', `/api/namespaces/${ns}/apps`).should((response) =>{
+    count = response.body.applications.filter((item) => item.name.includes(name)).length;
+  }); 
   cy.get('tbody').within(() => {
     cy.contains('No apps found').should('not.exist');
-    cy.get('tr').should('have.length', sees);
+    cy.get('tr').should('have.length', count);
   });
 });
 
diff --git a/frontend/cypress/integration/common/istio_config.ts b/frontend/cypress/integration/common/istio_config.ts
index e499b6e56..777c238a8 100644
--- a/frontend/cypress/integration/common/istio_config.ts
+++ b/frontend/cypress/integration/common/istio_config.ts
@@ -18,6 +18,25 @@ function labelsStringToJson(labelsString: string) {
   return `{${labelsJson}}`;
 }
 
+// I included this, because the URL parameter is in plural, but the the Type itself in Kiali is singular
+// This works for all of the types currently present in Kiali (Feb 8 2023), but may break in the future, because
+// it does not support all of the english words  
+function singularize(word:string) {
+  const endings = {
+      ves: 'fe',
+      ies: 'y',
+      i: 'us',
+      zes: 'ze',
+      ses: 's',
+      es: 'e',
+      s: ''
+  };
+  return word.replace(
+      new RegExp(`(${Object.keys(endings).join('|')})$`), 
+      r => endings[r]
+  );
+}
+
 function minimalAuthorizationPolicy(name: string, namespace: string): string {
   return `{
     "apiVersion": "security.istio.io/v1beta1",
@@ -301,6 +320,18 @@ Then('user only sees {string}', (sees: string) => {
   });
 });
 
+Then('only {string} are visible in the {string} namespace', (sees: string, ns:string) => {
+  let lowercaseSees:string = sees.charAt(0).toLowerCase() + sees.slice(1);
+  let count:number;
+  cy.request('GET', `/api/istio/config?objects=${lowercaseSees}&validate=true`).should((response) =>{
+    count = response.body[ns][lowercaseSees].length;
+  }); 
+  cy.get('tbody').contains('tr', singularize(sees));
+  cy.get('tbody').within(() => {
+    cy.get('tr').should('have.length', count);
+  });
+});
+
 Then('user sees {string}', (sees: string) => {
   cy.get('tbody').contains('tr', sees);
 });
@@ -342,9 +373,15 @@ Then('the AuthorizationPolicy should have a {string}', function(healthStatus: st
 });
 
 Then('the {string} {string} of the {string} namespace should have a {string}', function(crdInstanceName: string, crdName: string, namespace:string, healthStatus: string) {
-  cy.get(`[data-test=VirtualItem_Ns${namespace}_${crdName.toLowerCase()}_${crdInstanceName}] svg`)
-      .invoke('attr', 'style')
-      .should('have.string', `${healthStatus}-color`);
+  it('loading config list', { retries: 3 }, () => {
+    cy.request('GET', Cypress.config('baseUrl') + `/api/istio/config?refresh=0`);
+    cy.get('[data-test="refresh-button"]').click();
+    ensureKialiFinishedLoading();
+    cy.get(`[data-test=VirtualItem_Ns${namespace}_${crdName.toLowerCase()}_${crdInstanceName}] svg`, {timeout: 40000})
+        .should('be.visible')
+        .invoke('attr', 'style')
+        .should('have.string', `${healthStatus}-color`);
+  });
 });
 
 After({ tags: "@istio-page and @crd-validation" }, function () {
diff --git a/frontend/cypress/integration/common/istio_config_type_filters.ts b/frontend/cypress/integration/common/istio_config_type_filters.ts
index 4d6631e6c..ad7b2b41b 100644
--- a/frontend/cypress/integration/common/istio_config_type_filters.ts
+++ b/frontend/cypress/integration/common/istio_config_type_filters.ts
@@ -104,10 +104,12 @@ Then('the filter {string} should be visible only once', (category:string) => {
 });
 
 When("user chooses {int} type filters", (count:number) => {
+  cy.get('select[aria-label="filter_select_type"]').select('Istio Type');
   for (let i = 1; i <= count; i++) {
     cy.get('input[placeholder="Filter by Istio Type"]').click();
     cy.get(`[data-test=istio-type-dropdown] > :nth-child(${i})`)
     .should('be.visible').click();
+    cy.get('#loading_kiali_spinner').should('not.exist');
   };
 });
 
@@ -139,6 +141,7 @@ And("makes them all visible", () => {
 
 When("user clicks on {string}", (label:string) => {
   cy.get('#filter-selection > :nth-child(2)').contains(label).click();
+  cy.get('#loading_kiali_spinner').should('not.exist');
 });
 
 Then("he can see only {int} filters", (count:number) => {
diff --git a/frontend/cypress/integration/common/istio_config_validation_filters.ts b/frontend/cypress/integration/common/istio_config_validation_filters.ts
index 2fbfa2248..32144fada 100644
--- a/frontend/cypress/integration/common/istio_config_validation_filters.ts
+++ b/frontend/cypress/integration/common/istio_config_validation_filters.ts
@@ -51,7 +51,11 @@ Then('the validation filter {string} is no longer active', (category:string) =>
 });
 
 When("user chooses {int} validation filters", (count:number) => {
+  cy.get('select[aria-label="filter_select_type"]', { timeout: 1000 }).should('be.visible').select('Config');
+  cy.get('select[aria-label="filter_select_value"]', { timeout: 2000 }).should('be.visible');
   for (let i = 1; i <= count; i++) {
     cy.get('select[aria-label="filter_select_value"]').select(i);
+    cy.get('#loading_kiali_spinner')
+        .should('not.exist');
   };
 });
diff --git a/frontend/cypress/integration/common/label_check.ts b/frontend/cypress/integration/common/label_check.ts
index 894cbf37b..2e1858a0f 100644
--- a/frontend/cypress/integration/common/label_check.ts
+++ b/frontend/cypress/integration/common/label_check.ts
@@ -1,5 +1,5 @@
 export function activeFilters(count:number){
-  cy.get('#filter-selection > :nth-child(2)').find('[aria-label="close"]')
+  cy.get('#filter-selection > :nth-child(2)', { timeout: 1000 }).should('be.visible').find('[data-ouia-component-id="close"]')
     .each(() => {
     })
     .then(($lis) => {
@@ -9,5 +9,6 @@ export function activeFilters(count:number){
 
 export function showMore(){
   cy.get('#filter-selection > :nth-child(2)').contains('more').click();
+  cy.get('#filter-selection > :nth-child(2)').contains('more').should('not.exist');
 }
 
diff --git a/frontend/cypress/integration/common/overview.ts b/frontend/cypress/integration/common/overview.ts
index 05b1bf062..f308aa417 100644
--- a/frontend/cypress/integration/common/overview.ts
+++ b/frontend/cypress/integration/common/overview.ts
@@ -191,9 +191,10 @@ When('user hovers over the MinTLS locker', view => {
   .should('exist');
 });
 
-When ('user clicks the toggle on the right side of the {string} namespace card', (ns:string) => {
+Then ('the toggle on the right side of the {string} namespace card exists', (ns:string) => {
   ensureKialiFinishedLoading(); 
-  cy.get('article[data-test^="' + ns + '"]').find('button').click();
+  cy.get('article[data-test^="' + ns + '"]')
+  .should('exist');
 });
 
 Then('the user sees the certificates information', view => {
@@ -220,19 +221,6 @@ Then("the user sees information related to canary upgrades", view => {
       .should('exist');
 });
 
-Then('user can see links to external services',() =>{
-  cy.get('article[data-test^="istio-system"]').as("plane").within(() => {
-    cy.request('GET', '/api/grafana').should(response => {
-      expect(response.status).to.equal(200);
-      response.body.externalLinks.forEach(elem => {
-        if (elem.name != 'Istio Service Dashboard' && elem.name != 'Istio Workload Dashboard'){
-          cy.get('@plane').find('a').contains(elem.name);
-        }
-      });
-    });
-  });
-});
-
 And('user sees the {string} label in the {string} namespace card', (label: string, ns: string) => {
   cy.log(label);
   cy.get('article[data-test^="' + ns + '"]')
diff --git a/frontend/cypress/integration/common/wizard_istio_config.ts b/frontend/cypress/integration/common/wizard_istio_config.ts
new file mode 100644
index 000000000..7a4035ea5
--- /dev/null
+++ b/frontend/cypress/integration/common/wizard_istio_config.ts
@@ -0,0 +1,52 @@
+import { After, And, Given, Then, When } from '@badeball/cypress-cucumber-preprocessor';
+import { getColWithRowText } from './table';
+import { ensureKialiFinishedLoading } from "./transition";
+
+When('user clicks in the {string} Istio config actions', (action) => {
+  cy.get('button[data-test="config-actions-dropdown"]')
+      .click()
+      .get('#loading_kiali_spinner')
+      .should('not.exist');
+
+  cy.get('a[data-test="create_' + action + '"]')
+      .click()
+      .get('#loading_kiali_spinner')
+      .should('not.exist');
+});
+
+And('user sees the {string} config wizard', (title) => {
+  cy.get('h1').should('contain.text', title);
+});
+
+And('user adds listener', () => {
+  cy.get('button[name="addListener"]').click()
+});
+
+And('user types {string} in the name input', (name) => {
+  cy.get('input[id="name"]').type(name);
+});
+
+And('user types {string} in the add listener name input', (name) => {
+  cy.get('input[id="addName"]').type(name);
+});
+
+And('user types {string} in the add hostname input', (host) => {
+  cy.get('input[id="addHostname"]').type(host);
+});
+
+And('user types {string} in the add port input', (port) => {
+  cy.get('input[id="addPort"]').type(port);
+});
+
+And('user creates the istio config', () => {
+  cy.get('button[data-test="create"]')
+      .click()
+  it('spinner should disappear', { retries: 3 }, () => {
+    cy.get('#loading_kiali_spinner')
+        .should('not.exist');
+  });
+});
+
+Then('the K8sGateway {string} should be listed in {string} namespace', function(name, namespace: string) {
+  cy.get(`[data-test=VirtualItem_Ns${namespace}_k8sgateway_${name}] svg`).should('exist');
+});
diff --git a/frontend/cypress/integration/common/wizard_request_routing.ts b/frontend/cypress/integration/common/wizard_request_routing.ts
index e4f20343f..edbe41274 100644
--- a/frontend/cypress/integration/common/wizard_request_routing.ts
+++ b/frontend/cypress/integration/common/wizard_request_routing.ts
@@ -32,16 +32,25 @@ When('user clicks in the {string} actions', (action) => {
         case 'Request Routing':
             actionId = 'request_routing';
             break;
+        case 'K8s Gateway API Routing':
+            actionId = 'k8s_request_routing';
+            break;
         case 'Delete Traffic Routing':
             actionId = 'delete_traffic_routing';
             break;
     }
+    it('spinner should disappear', { retries: 3 }, () => {
+        cy.get('#loading_kiali_spinner')
+            .should('not.exist');
+    });
     cy.get('button[data-test="wizard-actions"]')
+        .should('exist')
         .click()
         .get('#loading_kiali_spinner')
         .should('not.exist');
 
     cy.get('button[data-test="' + actionId + '"]')
+        .should('exist')
         .click()
         .get('#loading_kiali_spinner')
         .should('not.exist');
@@ -64,11 +73,24 @@ And('user clicks in the {string} request matching dropdown', (select) => {
         .click();
 });
 
+And('user clicks in the {string} request filtering dropdown', (select) => {
+    cy.get('button[data-test="filtering-type-toggle"]')
+        .click();
+
+    cy.get('button[data-test="filtering-type-' + select + '"]')
+        .click();
+});
+
 And('user types {string} in the matching header input', (header) => {
     cy.get('input[id="header-name-id"]')
         .type(header);
 });
 
+And('user types {string} in the filtering header input', (header) => {
+    cy.get('input[id="filter-header-name-id"]')
+        .type(header);
+});
+
 And('user clicks in the {string} match value dropdown', (value) => {
     cy.get('button[data-test="requestmatching-match-toggle"]')
         .click();
@@ -87,6 +109,11 @@ And('user adds a match', () => {
         .click();
 });
 
+And('user adds a filter', () => {
+    cy.get('button[data-test="add-filter"]')
+        .click();
+});
+
 And('user types {string} traffic weight in the {string} workload', (weight, workload) => {
     cy.get('input[data-test="input-slider-' + workload + '"]')
         .type(weight);
@@ -117,8 +144,10 @@ And('user creates the configuration', () => {
 
     cy.get('button[data-test="confirm-create"]')
         .click()
-        .get('#loading_kiali_spinner')
-        .should('not.exist');
+    it('spinner should disappear', { retries: 3 }, () => {
+        cy.get('#loading_kiali_spinner')
+            .should('not.exist');
+    });
 });
 
 And('user updates the configuration', () => {
@@ -127,15 +156,19 @@ And('user updates the configuration', () => {
 
     cy.get('button[data-test="confirm-update"]')
         .click()
-        .get('#loading_kiali_spinner')
-        .should('not.exist');
+    it('spinner should disappear', { retries: 3 }, () => {
+        cy.get('#loading_kiali_spinner')
+            .should('not.exist');
+    });
 });
 
 And('user confirms delete the configuration', () => {
     cy.get('button[data-test="confirm-delete"]')
         .click()
-        .get('#loading_kiali_spinner')
-        .should('not.exist');
+    it('spinner should disappear', { retries: 3 }, () => {
+        cy.get('#loading_kiali_spinner')
+            .should('not.exist');
+    });
 });
 
 And('user sees the {string} {string} {string} reference', (namespace, name, type) => {
diff --git a/frontend/cypress/integration/featureFiles/apps.feature b/frontend/cypress/integration/featureFiles/apps.feature
index bd2279851..4e13e797a 100644
--- a/frontend/cypress/integration/featureFiles/apps.feature
+++ b/frontend/cypress/integration/featureFiles/apps.feature
@@ -50,7 +50,7 @@ Feature: Kiali Apps List page
   Scenario: Filter Applications table by Label
     When the user filters by "Label" for "app=reviews"
     Then user sees "reviews"
-    And user only sees 1 apps
+    And user only sees the apps with the "reviews" name in the "bookinfo" namespace
 
   @apps-page
   Scenario: The healthy status of a logical mesh application is reported in the list of applications
diff --git a/frontend/cypress/integration/featureFiles/graph_toolbar.feature b/frontend/cypress/integration/featureFiles/graph_toolbar.feature
index 428b1374e..df3903e88 100644
--- a/frontend/cypress/integration/featureFiles/graph_toolbar.feature
+++ b/frontend/cypress/integration/featureFiles/graph_toolbar.feature
@@ -40,6 +40,7 @@ Scenario: Disable all traffic
 # todo: would be a better test if demos has tcp and/or grpc traffic
 @graph-page-toolbar
 Scenario: Enable http traffic
+  When user disables all traffic
   When user enables "http" traffic
   Then user "sees" "http" traffic
   And user "does not see" "tcp" traffic
diff --git a/frontend/cypress/integration/featureFiles/istio_config.feature b/frontend/cypress/integration/featureFiles/istio_config.feature
index f4501d887..c3fd63794 100644
--- a/frontend/cypress/integration/featureFiles/istio_config.feature
+++ b/frontend/cypress/integration/featureFiles/istio_config.feature
@@ -23,7 +23,7 @@ Feature: Kiali Istio Config page
 
   Scenario: Filter Istio Config objects by Istio Type
     When the user filters by "Istio Type" for "Gateway"
-    Then user only sees "bookinfo-gateway"
+    Then only "Gateways" are visible in the "bookinfo" namespace
 
   Scenario: Filter Istio Config objects by Valid configuration
     When the user filters by "Config" for "Valid"
diff --git a/frontend/cypress/integration/featureFiles/overview.feature b/frontend/cypress/integration/featureFiles/overview.feature
index 390ecb524..d9db22374 100644
--- a/frontend/cypress/integration/featureFiles/overview.feature
+++ b/frontend/cypress/integration/featureFiles/overview.feature
@@ -120,5 +120,4 @@ Feature: Kiali Overview page
     Then user sees the "istio-system" namespace card
     And user sees the "Control plane" label in the "istio-system" namespace card
     And user sees the "Outbound policy" label in the "istio-system" namespace card
-    When user clicks the toggle on the right side of the "istio-system" namespace card
-    Then user can see links to external services
+    Then the toggle on the right side of the "istio-system" namespace card exists
diff --git a/frontend/cypress/integration/featureFiles/wizard_istio_config.feature b/frontend/cypress/integration/featureFiles/wizard_istio_config.feature
new file mode 100644
index 000000000..7b06136dd
--- /dev/null
+++ b/frontend/cypress/integration/featureFiles/wizard_istio_config.feature
@@ -0,0 +1,24 @@
+@istio-page
+Feature: Kiali Istio Config page
+
+  On the Istio Config page, an admin should see all the Istio Config objects.
+  The admin should be able to filter for the Istio Config objects they are looking for
+  and create new Istio objects.
+
+  Background:
+    Given user is at administrator perspective
+    And user is at the "istio" page
+    And user selects the "bookinfo" namespace
+
+  @wizard-istio-config
+  Scenario: Create a K8s Gateway scenario
+    And user clicks in the "K8sGateway" Istio config actions
+    And user sees the "Create K8sGateway" config wizard
+    And user adds listener
+    And user types "k8sapigateway" in the name input
+    And user types "listener" in the add listener name input
+    And user types "website.com" in the add hostname input
+    And user types "8080" in the add port input
+    And user previews the configuration
+    And user creates the istio config
+    Then the K8sGateway "k8sapigateway" should be listed in "bookinfo" namespace
diff --git a/frontend/cypress/integration/featureFiles/wizard_k8sgw_api_routing.feature b/frontend/cypress/integration/featureFiles/wizard_k8sgw_api_routing.feature
new file mode 100644
index 000000000..535c55b1b
--- /dev/null
+++ b/frontend/cypress/integration/featureFiles/wizard_k8sgw_api_routing.feature
@@ -0,0 +1,59 @@
+Feature: Service Details Wizard: K8s Gateway API Routing
+
+  User navigates to the service details page and open the Kiali Wizard to generate a K8s Gateway API Routing scenario.
+
+  Background:
+    Given user is at administrator perspective
+
+  @wizard-k8s-routing
+  Scenario: Create a K8s Gateway API Routing scenario
+    When user opens the namespace "bookinfo" and "reviews" service details page
+    And user clicks in the "K8s Gateway API Routing" actions
+    And user sees the "Create K8s Gateway API Routing" wizard
+    And user clicks in the "Request Matching" tab
+    And user clicks in the "headers" request matching dropdown
+    And user types "end-user" in the matching header input
+    And user clicks in the "Exact" match value dropdown
+    And user types "jason" in the match value input
+    And user adds a match
+    And user clicks in the "Route To" tab
+    And user types "100" traffic weight in the "reviews" workload
+    And user clicks in the "Route Filtering" tab
+    And user clicks in the "requestMirror" request filtering dropdown
+    And user adds a filter
+    And user adds a route
+    And user previews the configuration
+    And user creates the configuration
+    Then user sees the "Istio Config" table with 1 rows
+
+  @wizard-k8s-routing
+  Scenario: See a HTTPRoute generated
+    When user clicks in the "Istio Config" table "HTTP" badge "reviews" name row link
+    Then user sees the "kind: HTTPRoute" regex in the editor
+    And user sees the "bookinfo" "reviews" "service" reference
+
+  @wizard-k8s-routing
+  Scenario: Update a K8s Gateway API Routing scenario
+    When user opens the namespace "bookinfo" and "reviews" service details page
+    And user clicks in the "K8s Gateway API Routing" actions
+    And user sees the "Update K8s Gateway API Routing" wizard
+    And user clicks on "Show" Advanced Options
+    And user clicks in the "K8s Gateways" tab
+    And user clicks on Add Gateway
+    And user selects Create Gateway
+    And user previews the configuration
+    And user updates the configuration
+    Then user sees the "Istio Config" table with 2 rows
+
+  @wizard-k8s-routing
+  Scenario: See a K8s Gateway generated with warning
+    When user clicks in the "Istio Config" table "G" badge "reviews-gateway" name row link
+    Then user sees the "kind: Gateway" regex in the editor
+
+  ## @wizard-k8s-routing
+  Scenario: Delete the K8s Gateway Routing scenario
+    When user opens the namespace "bookinfo" and "reviews" service details page
+    And user clicks in the "Delete Traffic Routing" actions
+    And user confirms delete the configuration
+    Then user sees the "Istio Config" table with empty message
+
diff --git a/frontend/package.json b/frontend/package.json
index ac024442a..56423a0ba 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@kiali/kiali-ui",
-  "version": "1.61.0",
+  "version": "1.64.0",
   "description": "React UI for [Kiali](https://github.com/kiali/kiali).",
   "keywords": [
     "istio service mesh",
@@ -62,7 +62,6 @@
     "@patternfly/react-styles": "4.48.5",
     "@patternfly/react-table": "4.67.5",
     "@patternfly/react-tokens": "4.50.5",
-    "@webcomponents/custom-elements": "1.2.4",
     "axios": "0.21.4",
     "bootstrap-slider-without-jquery": "10.0.0",
     "cy-node-html-label": "2.0.0",
@@ -104,6 +103,8 @@
     "visibilityjs": "2.0.2"
   },
   "devDependencies": {
+    "@badeball/cypress-cucumber-preprocessor": "^12.2.0",
+    "@cypress/browserify-preprocessor": "3.0.2",
     "@types/cytoscape": "3.14.0",
     "@types/enzyme": "3.10.5",
     "@types/jest": "23.3.10",
@@ -116,8 +117,6 @@
     "@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
     "axios-mock-adapter": "1.16.0",
     "cypress": "^10.7.0",
-    "@badeball/cypress-cucumber-preprocessor": "^12.2.0",
-    "@cypress/browserify-preprocessor": "3.0.2",
     "cypress-multi-reporters": "^1.6.0",
     "cypress-react-selector": "^2.3.17",
     "enzyme": "3.11.0",
@@ -151,7 +150,7 @@
   },
   "husky": {
     "hooks": {
-      "pre-commit": "yarn run pretty-quick --staged --pattern \"{src/**/*.{js,jsx,ts,tsx,json,yml,css,scss},travis.yml,*.json}\" && npm run lint:precommit"
+      "pre-commit": "yarn run pretty-quick --staged --pattern \"{frontend/src/**/*.{js,jsx,ts,tsx,json,yml,css,scss},travis.yml,*.json}\" && npm run lint:precommit"
     }
   },
   "browserslist": [
diff --git a/frontend/reporter-config.json b/frontend/reporter-config.json
index 2ce667f13..417c8973a 100644
--- a/frontend/reporter-config.json
+++ b/frontend/reporter-config.json
@@ -4,4 +4,3 @@
     "mochaFile": "cypress/results/results-[hash].xml"
   }
 }
-
diff --git a/frontend/src/actions/GlobalActions.ts b/frontend/src/actions/GlobalActions.ts
index 0818ed72d..44be91018 100644
--- a/frontend/src/actions/GlobalActions.ts
+++ b/frontend/src/actions/GlobalActions.ts
@@ -9,7 +9,7 @@ export const GlobalActions = {
   decrementLoadingCounter: createAction(ActionKeys.DECREMENT_LOADING_COUNTER),
   setPageVisibilityHidden: createAction(ActionKeys.SET_PAGE_VISIBILITY_HIDDEN),
   setPageVisibilityVisible: createAction(ActionKeys.SET_PAGE_VISIBILITY_VISIBLE),
-  setKiosk: createStandardAction(ActionKeys.SET_KIOSK)<KioskMode>(),
+  setKiosk: createStandardAction(ActionKeys.SET_KIOSK)<KioskMode>()
 };
 
 export type GlobalAction = ActionType<typeof GlobalActions>;
diff --git a/frontend/src/actions/GraphActions.ts b/frontend/src/actions/GraphActions.ts
index a7e8ab055..07af3a30a 100644
--- a/frontend/src/actions/GraphActions.ts
+++ b/frontend/src/actions/GraphActions.ts
@@ -6,7 +6,7 @@ import { TimeInMilliseconds } from 'types/Common';
 export const GraphActions = {
   onNamespaceChange: createAction(ActionKeys.GRAPH_ON_NAMESPACE_CHANGE),
   setEdgeMode: createStandardAction(ActionKeys.GRAPH_SET_EDGE_MODE)<EdgeMode>(),
-  setGraphDefinition: createStandardAction(ActionKeys.GRAPH_SET_DEFINITION)<GraphDefinition>(),  
+  setGraphDefinition: createStandardAction(ActionKeys.GRAPH_SET_DEFINITION)<GraphDefinition>(),
   setLayout: createStandardAction(ActionKeys.GRAPH_SET_LAYOUT)<Layout>(),
   setNamespaceLayout: createStandardAction(ActionKeys.GRAPH_SET_NAMESPACE_LAYOUT)<Layout>(),
   setNode: createStandardAction(ActionKeys.GRAPH_SET_NODE)<NodeParamsType | undefined>(),
diff --git a/frontend/src/actions/GraphThunkActions.ts b/frontend/src/actions/GraphThunkActions.ts
index 3d81a3be7..90ed36f98 100644
--- a/frontend/src/actions/GraphThunkActions.ts
+++ b/frontend/src/actions/GraphThunkActions.ts
@@ -1,4 +1,4 @@
-import { KialiDispatch } from "../types/Redux";
+import { KialiDispatch } from '../types/Redux';
 import { GraphActions } from './GraphActions';
 
 const GraphThunkActions = {
diff --git a/frontend/src/actions/HelpDropdownActions.ts b/frontend/src/actions/HelpDropdownActions.ts
index efe263af3..e75a20708 100644
--- a/frontend/src/actions/HelpDropdownActions.ts
+++ b/frontend/src/actions/HelpDropdownActions.ts
@@ -7,7 +7,8 @@ export const HelpDropdownActions = {
     resolve({
       status: status.status,
       externalServices: status.externalServices,
-      warningMessages: status.warningMessages
+      warningMessages: status.warningMessages,
+      istioEnvironment: status.istioEnvironment
     })
   )
 };
diff --git a/frontend/src/actions/JaegerThunkActions.ts b/frontend/src/actions/JaegerThunkActions.ts
index b35323d07..1aa75793c 100644
--- a/frontend/src/actions/JaegerThunkActions.ts
+++ b/frontend/src/actions/JaegerThunkActions.ts
@@ -2,7 +2,7 @@ import { AxiosError } from 'axios';
 
 import * as AlertUtils from '../utils/AlertUtils';
 import * as API from '../services/Api';
-import { KialiDispatch } from "../types/Redux";
+import { KialiDispatch } from '../types/Redux';
 import { JaegerActions } from './JaegerActions';
 import { setTraceId as setURLTraceId } from 'utils/SearchParamUtils';
 import transformTraceData from 'utils/tracing/TraceTransform';
diff --git a/frontend/src/actions/MessageCenterThunkActions.ts b/frontend/src/actions/MessageCenterThunkActions.ts
index 4d697b721..6d21dbdab 100644
--- a/frontend/src/actions/MessageCenterThunkActions.ts
+++ b/frontend/src/actions/MessageCenterThunkActions.ts
@@ -1,6 +1,6 @@
 import { MessageCenterActions } from './MessageCenterActions';
 import { KialiAppState } from '../store/Store';
-import { KialiDispatch } from "../types/Redux";
+import { KialiDispatch } from '../types/Redux';
 
 const MessageCenterThunkActions = {
   toggleMessageCenter: () => {
diff --git a/frontend/src/actions/NamespaceThunkActions.ts b/frontend/src/actions/NamespaceThunkActions.ts
index 02a6150dd..5e2ef5ac3 100644
--- a/frontend/src/actions/NamespaceThunkActions.ts
+++ b/frontend/src/actions/NamespaceThunkActions.ts
@@ -1,6 +1,6 @@
 import { KialiAppState } from '../store/Store';
 import * as Api from '../services/Api';
-import { KialiDispatch } from "../types/Redux";
+import { KialiDispatch } from '../types/Redux';
 import { NamespaceActions } from './NamespaceAction';
 
 const shouldFetchNamespaces = (state: KialiAppState) => {
diff --git a/frontend/src/actions/UserSettingsThunkActions.ts b/frontend/src/actions/UserSettingsThunkActions.ts
index 136a361fa..d66f4f37d 100644
--- a/frontend/src/actions/UserSettingsThunkActions.ts
+++ b/frontend/src/actions/UserSettingsThunkActions.ts
@@ -1,4 +1,4 @@
-import { KialiDispatch } from "../types/Redux";
+import { KialiDispatch } from '../types/Redux';
 import { UserSettingsActions } from './UserSettingsActions';
 
 const UserSettingsThunkActions = {
diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx
index 7012ebaed..599b2fdba 100644
--- a/frontend/src/app/App.tsx
+++ b/frontend/src/app/App.tsx
@@ -1,18 +1,18 @@
 import axios from 'axios';
 import * as React from 'react';
-import {PersistGate} from 'redux-persist/lib/integration/react';
-import {Provider} from 'react-redux';
-import {Router, withRouter} from 'react-router-dom';
+import { PersistGate } from 'redux-persist/lib/integration/react';
+import { Provider } from 'react-redux';
+import { Router, withRouter } from 'react-router-dom';
 import * as Visibility from 'visibilityjs';
-import {GlobalActions} from '../actions/GlobalActions';
+import { GlobalActions } from '../actions/GlobalActions';
 import NavigationContainer from '../components/Nav/Navigation';
-import {persistor, store} from '../store/ConfigStore';
+import { persistor, store } from '../store/ConfigStore';
 import AuthenticationControllerContainer from './AuthenticationController';
 import history from './History';
 import InitializingScreen from './InitializingScreen';
 import StartupInitializer from './StartupInitializer';
 import LoginPageContainer from '../pages/Login/LoginPage';
-import {LoginActions} from '../actions/LoginActions';
+import { LoginActions } from '../actions/LoginActions';
 import 'tippy.js/dist/tippy.css';
 import 'tippy.js/dist/themes/light-border.css';
 import 'react-datepicker/dist/react-datepicker.css';
@@ -46,7 +46,7 @@ const decrementLoadingCounter = () => {
 const openshift_token: string | null = (function () {
   const urlParams = new URLSearchParams(document.location.search);
   return urlParams.get('oauth_token');
-}());
+})();
 
 // intercept all Axios requests and dispatch the INCREMENT_LOADING_COUNTER Action
 axios.interceptors.request.use(
diff --git a/frontend/src/app/AuthenticationController.tsx b/frontend/src/app/AuthenticationController.tsx
index 88846f2ba..c26a65efd 100644
--- a/frontend/src/app/AuthenticationController.tsx
+++ b/frontend/src/app/AuthenticationController.tsx
@@ -99,11 +99,11 @@ export class AuthenticationController extends React.Component<
         dispatchLoginCycleOnLoad = true;
       }
 
-      // If login strategy is Openshift, OpenId, check if there is an
+      // If login strategy is Openshift, check if there is an
       // "access_token" or "id_token" hash parameter in the URL. If there is,
       // this means the IdP is calling back. Dispatch the login cycle to finish
       // the authentication.
-      if (isAuthStrategyOAuth()) {
+      if (authenticationConfig.strategy === AuthStrategy.openshift) {
         const pattern = /[#&](access_token|id_token)=/;
         dispatchLoginCycleOnLoad = pattern.test(window.location.hash);
       }
@@ -297,7 +297,7 @@ export class AuthenticationController extends React.Component<
     }
   }
 
-  // Check which clusters does not have an accessible Kiali instance.
+  // Check which clusters do not have an accessible Kiali instance.
   // Emit a warning telling that for those clusters, no cross-links will be available.
   private checkConfiguredRemoteKialis(backendConfigs: ServerConfig) {
     if (backendConfigs.clusters) {
diff --git a/frontend/src/components/About/AboutUIModal.tsx b/frontend/src/components/About/AboutUIModal.tsx
index 8073e81bd..f88e26770 100644
--- a/frontend/src/components/About/AboutUIModal.tsx
+++ b/frontend/src/components/About/AboutUIModal.tsx
@@ -50,8 +50,9 @@ class AboutUIModal extends React.Component<AboutUIModalProps, AboutUIModalState>
         ? this.props.status[StatusKey.KIALI_CORE_VERSION]
         : `${this.props.status[StatusKey.KIALI_CORE_VERSION]} (${this.props.status[StatusKey.KIALI_CORE_COMMIT_HASH]})`;
     const containerVersion = this.props.status[StatusKey.KIALI_CONTAINER_VERSION];
-    const meshVersion = this.props.status[StatusKey.MESH_NAME] ? `${this.props.status[StatusKey.MESH_NAME]} ${this.props.status[StatusKey.MESH_VERSION] || ''}` : 'Unknown';
-
+    const meshVersion = this.props.status[StatusKey.MESH_NAME]
+      ? `${this.props.status[StatusKey.MESH_NAME]} ${this.props.status[StatusKey.MESH_VERSION] || ''}`
+      : 'Unknown';
 
     return (
       <AboutModal
@@ -85,7 +86,7 @@ class AboutUIModal extends React.Component<AboutUIModalProps, AboutUIModalState>
           </TextList>
         </TextContent>
         {this.props.warningMessages.length > 0 && (
-          <Alert variant="warning" title={this.props.warningMessages[0]} style={{marginTop: '1em'}} />
+          <Alert variant="warning" title={this.props.warningMessages[0]} style={{ marginTop: '1em' }} />
         )}
         <TextContent>
           <Title headingLevel="h3" size={TitleSizes.xl} style={{ padding: '20px 0px 20px' }}>
diff --git a/frontend/src/components/Charts/Container.tsx b/frontend/src/components/Charts/Container.tsx
index a3597228e..a4eda3702 100644
--- a/frontend/src/components/Charts/Container.tsx
+++ b/frontend/src/components/Charts/Container.tsx
@@ -22,9 +22,14 @@ const formatValue = (label: string, datum: RichDataPoint, value: number, y0?: nu
   // Scale factor is usually undefined, except when a second axis is in use (then it's the ratio between first axis and second axis maxs)
 
   if (y0 !== undefined) {
-    return label + ':' +
-      ' source = ' + getFormatter(d3Format, datum.unit!, true)(value / (datum.scaleFactor || 1)) +
-      '; destination = ' + getFormatter(d3Format, datum.unit!, true)(y0 / (datum.scaleFactor || 1));
+    return (
+      label +
+      ':' +
+      ' source = ' +
+      getFormatter(d3Format, datum.unit!, true)(value / (datum.scaleFactor || 1)) +
+      '; destination = ' +
+      getFormatter(d3Format, datum.unit!, true)(y0 / (datum.scaleFactor || 1))
+    );
   }
 
   return label + ': ' + getFormatter(d3Format, datum.unit!, true)(value / (datum.scaleFactor || 1));
diff --git a/frontend/src/components/Charts/CustomTooltip.tsx b/frontend/src/components/Charts/CustomTooltip.tsx
index 32815febe..8e6b108eb 100644
--- a/frontend/src/components/Charts/CustomTooltip.tsx
+++ b/frontend/src/components/Charts/CustomTooltip.tsx
@@ -46,7 +46,7 @@ const getHeader = (activePoints?: VCDataPoint[]): string | undefined => {
     const x = activePoints[0].x;
     if (typeof x === 'object') {
       // Assume date
-      return x.toLocaleTimeString();
+      return x.toLocaleStringWithConditionalDate();
     }
   }
   return undefined;
diff --git a/frontend/src/components/Charts/SparklineChart.tsx b/frontend/src/components/Charts/SparklineChart.tsx
index 0234e6bdd..824df8317 100644
--- a/frontend/src/components/Charts/SparklineChart.tsx
+++ b/frontend/src/components/Charts/SparklineChart.tsx
@@ -8,11 +8,9 @@ import {
   ChartArea,
   ChartLabel,
   ChartLegend,
-  ChartThreshold,
-
+  ChartThreshold
 } from '@patternfly/react-charts';
 
-
 import { VCLines, VCDataPoint, RichDataPoint } from 'types/VictoryChartInfo';
 import { CustomTooltip } from './CustomTooltip';
 import { VCEvent, addLegendEvent } from 'utils/VictoryEvents';
@@ -136,13 +134,15 @@ export class SparklineChart extends React.Component<Props, State> {
         // Hack: 1 pxl on Y domain padding to prevent harsh clipping (https://github.com/kiali/kiali/issues/2069)
         domainPadding={{ y: 1 }}
       >
-        {this.props.showXAxisValues ?
-          <ChartAxis 
+        {this.props.showXAxisValues ? (
+          <ChartAxis
             tickValues={this.props.series[0].datapoints.map(dp => dp.x)}
-            tickFormat={(x => (x as Date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }))}
-            tickCount={2} /> :
+            tickFormat={x => (x as Date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+            tickCount={2}
+          />
+        ) : (
           <ChartAxis tickCount={15} style={hiddenAxisStyle} />
-        }
+        )}
         {this.props.showYAxis ? (
           <ChartAxis
             label={this.props.labelName}
@@ -203,19 +203,20 @@ export class SparklineChart extends React.Component<Props, State> {
           />
         )}
 
-        {this.props.thresholds && this.props.thresholds.map((serie, idx) => {
-          if (this.state.hiddenSeries.has(idx)) {
-            return undefined;
-          }
-          return (
-            <ChartThreshold
-              data={serie.datapoints}
-              style={{
-                data: { stroke: serie.color }
-              }}
-            />
-          );
-        })}
+        {this.props.thresholds &&
+          this.props.thresholds.map((serie, idx) => {
+            if (this.state.hiddenSeries.has(idx)) {
+              return undefined;
+            }
+            return (
+              <ChartThreshold
+                data={serie.datapoints}
+                style={{
+                  data: { stroke: serie.color }
+                }}
+              />
+            );
+          })}
       </Chart>
     );
   }
diff --git a/frontend/src/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx b/frontend/src/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx
index b605a0d1f..d77a18b90 100644
--- a/frontend/src/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx
+++ b/frontend/src/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx
@@ -2,13 +2,13 @@ import * as React from 'react';
 import { connect } from 'react-redux';
 import { Link } from 'react-router-dom';
 import { style } from 'typestyle';
-import { Spinner, Tooltip, TooltipPosition } from "@patternfly/react-core";
+import { Spinner, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { ExternalLinkAltIcon } from '@patternfly/react-icons';
 
 import history from 'app/History';
 import { BoxByType, DecoratedGraphNodeData, NodeType } from 'types/Graph';
 import { JaegerInfo } from 'types/JaegerInfo';
-import { durationSelector } from "store/Selectors";
+import { durationSelector } from 'store/Selectors';
 import { KialiAppState } from 'store/Store';
 import { Paths, serverConfig } from 'config';
 import { NodeContextMenuProps } from '../CytoscapeContextMenu';
@@ -16,17 +16,13 @@ import { getTitle } from 'pages/Graph/SummaryPanelCommon';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { renderBadgedName } from 'pages/Graph/SummaryLink';
 import { PFColors } from 'components/Pf/PfColors';
-import {
-  SERVICE_WIZARD_ACTIONS,
-  WIZARD_TITLES,
-  WizardAction
-} from "../../IstioWizards/WizardActions";
-import { DELETE_TRAFFIC_ROUTING } from "../../IstioWizards/ServiceWizardActionsDropdownGroup";
-import { isParentKiosk, kioskContextMenuAction } from "../../Kiosk/KioskActions";
-import { DurationInSeconds, TimeInMilliseconds } from "types/Common";
-import { useServiceDetailForGraphNode } from "../../../hooks/services";
-import { canDelete } from "../../../types/Permissions";
-import { getServiceDetailsUpdateLabel, hasServiceDetailsTrafficRouting } from "../../../types/ServiceInfo";
+import { SERVICE_WIZARD_ACTIONS, WIZARD_TITLES, WizardAction } from '../../IstioWizards/WizardActions';
+import { DELETE_TRAFFIC_ROUTING } from '../../IstioWizards/ServiceWizardActionsDropdownGroup';
+import { isParentKiosk, kioskContextMenuAction } from '../../Kiosk/KioskActions';
+import { DurationInSeconds, TimeInMilliseconds } from 'types/Common';
+import { useServiceDetailForGraphNode } from '../../../hooks/services';
+import { canDelete } from '../../../types/Permissions';
+import { getServiceDetailsUpdateLabel, hasServiceDetailsTrafficRouting } from '../../../types/ServiceInfo';
 
 type ReduxProps = {
   duration: DurationInSeconds;
@@ -106,7 +102,12 @@ function getLinkParamsForNode(node: DecoratedGraphNodeData): LinkParams | undefi
 }
 
 export function NodeContextMenu(props: Props) {
-  const [serviceDetails, gateways, peerAuthentications, isServiceDetailsLoading] = useServiceDetailForGraphNode(props, true, props.duration, props.updateTime);
+  const [serviceDetails, gateways, peerAuthentications, isServiceDetailsLoading] = useServiceDetailForGraphNode(
+    props,
+    true,
+    props.duration,
+    props.updateTime
+  );
   const updateLabel = getServiceDetailsUpdateLabel(serviceDetails);
 
   // TODO: Deduplicate
@@ -138,7 +139,7 @@ export function NodeContextMenu(props: Props) {
       // Kiosk actions are used when the kiosk specifies a parent,
       // otherwise the kiosk=true will keep the links inside Kiali
       if (isParentKiosk(props.kiosk)) {
-        item =
+        item = (
           <Link
             to={''}
             onClick={() => {
@@ -146,7 +147,8 @@ export function NodeContextMenu(props: Props) {
             }}
             className={commonLinkProps.className}
             children={commonLinkProps.children}
-          />;
+          />
+        );
       } else {
         item = <Link to={href} {...commonLinkProps} />;
       }
@@ -168,7 +170,14 @@ export function NodeContextMenu(props: Props) {
     props.contextMenu.hide(0);
 
     if (props.onLaunchWizard && serviceDetails && gateways && peerAuthentications) {
-      props.onLaunchWizard(eventKey, updateLabel.length === 0 ? 'create' : 'update', props.namespace, serviceDetails, gateways, peerAuthentications);
+      props.onLaunchWizard(
+        eventKey,
+        updateLabel.length === 0 ? 'create' : 'update',
+        props.namespace,
+        serviceDetails,
+        gateways,
+        peerAuthentications
+      );
     }
   }
 
@@ -197,7 +206,9 @@ export function NodeContextMenu(props: Props) {
   }
 
   function renderWizardActionItem(eventKey: string) {
-    const enabledItem = !hasServiceDetailsTrafficRouting(serviceDetails) || (hasServiceDetailsTrafficRouting(serviceDetails) && updateLabel === eventKey);
+    const enabledItem =
+      !hasServiceDetailsTrafficRouting(serviceDetails) ||
+      (hasServiceDetailsTrafficRouting(serviceDetails) && updateLabel === eventKey);
 
     // An Item is enabled under two conditions:
     // a) No traffic -> Wizard can create new one
@@ -205,16 +216,21 @@ export function NodeContextMenu(props: Props) {
     // Otherwise, the item should be disabled
     if (!enabledItem) {
       return (
-        <div key={eventKey} className={contextMenuItem} style={{color: '#d2d2d2'}}>
+        <div key={eventKey} className={contextMenuItem} style={{ color: '#d2d2d2' }}>
           <Tooltip position={TooltipPosition.left} content={<>{getDropdownItemTooltipMessage()}</>}>
             <div style={{ display: 'inline-block', cursor: 'not-allowed' }}>{WIZARD_TITLES[eventKey]}</div>
           </Tooltip>
         </div>
-      )
+      );
     } else {
       return (
         <div key={eventKey} className={contextMenuItem} data-test={eventKey + '_action'}>
-          <a href="#" rel="noreferrer noopener" className={contextMenuItemLink} onClick={(e) => handleClickWizard(e, eventKey as WizardAction)}>
+          <a
+            href="#"
+            rel="noreferrer noopener"
+            className={contextMenuItemLink}
+            onClick={e => handleClickWizard(e, eventKey as WizardAction)}
+          >
             {WIZARD_TITLES[eventKey]}
           </a>
         </div>
@@ -223,9 +239,12 @@ export function NodeContextMenu(props: Props) {
   }
 
   function renderDeleteTrafficRoutingItem() {
-    if (!canDelete(serviceDetails?.istioPermissions) || !hasServiceDetailsTrafficRouting(serviceDetails) /*|| props.isDisabled*/) {
+    if (
+      !canDelete(serviceDetails?.istioPermissions) ||
+      !hasServiceDetailsTrafficRouting(serviceDetails) /*|| props.isDisabled*/
+    ) {
       return (
-        <div className={contextMenuItem} style={{color: '#d2d2d2'}}>
+        <div className={contextMenuItem} style={{ color: '#d2d2d2' }}>
           <Tooltip position={TooltipPosition.left} content={<>{getDropdownItemTooltipMessage()}</>}>
             <div style={{ display: 'inline-block', cursor: 'not-allowed' }}>Delete Traffic Routing</div>
           </Tooltip>
@@ -234,7 +253,13 @@ export function NodeContextMenu(props: Props) {
     } else {
       return (
         <div className={contextMenuItem}>
-          <a href="#" rel="noreferrer noopener" className={contextMenuItemLink} onClick={handleDeleteTrafficRouting} data-test="delete-traffic-routing">
+          <a
+            href="#"
+            rel="noreferrer noopener"
+            className={contextMenuItemLink}
+            onClick={handleDeleteTrafficRouting}
+            data-test="delete-traffic-routing"
+          >
             Delete Traffic Routing
           </a>
         </div>
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeContextMenu.tsx b/frontend/src/components/CytoscapeGraph/CytoscapeContextMenu.tsx
index c1d89e1c6..a4bbb72a1 100644
--- a/frontend/src/components/CytoscapeGraph/CytoscapeContextMenu.tsx
+++ b/frontend/src/components/CytoscapeGraph/CytoscapeContextMenu.tsx
@@ -4,20 +4,28 @@ import * as Cy from 'cytoscape';
 import { Router } from 'react-router';
 import tippy, { Instance } from 'tippy.js';
 import { DecoratedGraphEdgeData, DecoratedGraphNodeData } from '../../types/Graph';
-import { PeerAuthentication } from "../../types/IstioObjects";
-import { ServiceDetailsInfo } from "../../types/ServiceInfo";
+import { PeerAuthentication } from '../../types/IstioObjects';
+import { ServiceDetailsInfo } from '../../types/ServiceInfo';
 import { Provider } from 'react-redux';
 import { store } from '../../store/ConfigStore';
 import history from '../../app/History';
 import { getOptions } from './ContextMenu/NodeContextMenu';
-import { WizardAction, WizardMode } from "../IstioWizards/WizardActions";
+import { WizardAction, WizardMode } from '../IstioWizards/WizardActions';
 
 export type EdgeContextMenuProps = DecoratedGraphEdgeData & ContextMenuProps;
 export type EdgeContextMenuComponentType = React.ComponentType<EdgeContextMenuProps>;
-export type NodeContextMenuProps = DecoratedGraphNodeData & ContextMenuProps & {
-  onLaunchWizard?: (key: WizardAction, mode: WizardMode, namespace: string, serviceDetails: ServiceDetailsInfo, gateways: string[], peerAuths: PeerAuthentication[]) => void;
-  onDeleteTrafficRouting?: (key: string, serviceDetails: ServiceDetailsInfo) => void;
-};
+export type NodeContextMenuProps = DecoratedGraphNodeData &
+  ContextMenuProps & {
+    onLaunchWizard?: (
+      key: WizardAction,
+      mode: WizardMode,
+      namespace: string,
+      serviceDetails: ServiceDetailsInfo,
+      gateways: string[],
+      peerAuths: PeerAuthentication[]
+    ) => void;
+    onDeleteTrafficRouting?: (key: string, serviceDetails: ServiceDetailsInfo) => void;
+  };
 export type NodeContextMenuComponentType = React.ComponentType<NodeContextMenuProps>;
 export type ContextMenuComponentType = EdgeContextMenuComponentType | NodeContextMenuComponentType;
 
@@ -25,7 +33,14 @@ type Props = {
   contextMenuEdgeComponent?: EdgeContextMenuComponentType;
   contextMenuNodeComponent?: NodeContextMenuComponentType;
   onDeleteTrafficRouting?: (key: string, serviceDetails: ServiceDetailsInfo) => void;
-  onLaunchWizard?: (key: WizardAction, mode: WizardMode, namespace: string, serviceDetails: ServiceDetailsInfo, gateways: string[], peerAuths: PeerAuthentication[]) => void;
+  onLaunchWizard?: (
+    key: WizardAction,
+    mode: WizardMode,
+    namespace: string,
+    serviceDetails: ServiceDetailsInfo,
+    gateways: string[],
+    peerAuths: PeerAuthentication[]
+  ) => void;
 };
 
 type TippyInstance = Instance;
@@ -151,7 +166,9 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent<Props> {
       }
     ).instances[0];
 
-    let menuComponent = (<ContextMenuComponentType element={target} contextMenu={tippyInstance} isHover={isHover} {...target.data()} />);
+    let menuComponent = (
+      <ContextMenuComponentType element={target} contextMenu={tippyInstance} isHover={isHover} {...target.data()} />
+    );
     if (isNode) {
       menuComponent = (
         <ContextMenuComponentType
@@ -160,15 +177,14 @@ export class CytoscapeContextMenuWrapper extends React.PureComponent<Props> {
           isHover={isHover}
           onDeleteTrafficRouting={this.props.onDeleteTrafficRouting}
           onLaunchWizard={this.props.onLaunchWizard}
-          {...target.data()} />
+          {...target.data()}
+        />
       );
     }
 
     const result = (
       <Provider store={store}>
-        <Router history={history}>
-          {menuComponent}
-        </Router>
+        <Router history={history}>{menuComponent}</Router>
       </Provider>
     );
 
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeGraph.tsx b/frontend/src/components/CytoscapeGraph/CytoscapeGraph.tsx
index a1100dbf8..caebd31b5 100644
--- a/frontend/src/components/CytoscapeGraph/CytoscapeGraph.tsx
+++ b/frontend/src/components/CytoscapeGraph/CytoscapeGraph.tsx
@@ -25,7 +25,7 @@ import { JaegerTrace } from 'types/JaegerInfo';
 import Namespace from '../../types/Namespace';
 import { addInfo } from 'utils/AlertUtils';
 import { angleBetweenVectors, squaredDistance, normalize } from '../../utils/MathUtils';
-import { WizardAction, WizardMode } from "../IstioWizards/WizardActions";
+import { WizardAction, WizardMode } from '../IstioWizards/WizardActions';
 import {
   CytoscapeContextMenuWrapper,
   NodeContextMenuComponentType,
@@ -43,8 +43,8 @@ import { serverConfig } from 'config';
 import { decoratedNodeData } from './CytoscapeGraphUtils';
 import { scoreNodes, ScoringCriteria } from './GraphScore';
 import { assignEdgeHealth } from 'types/ErrorRate/GraphEdgeStatus';
-import { PeerAuthentication } from "types/IstioObjects";
-import { ServiceDetailsInfo } from "types/ServiceInfo";
+import { PeerAuthentication } from 'types/IstioObjects';
+import { ServiceDetailsInfo } from 'types/ServiceInfo';
 
 type CytoscapeGraphProps = {
   compressOnHide: boolean;
@@ -63,7 +63,14 @@ type CytoscapeGraphProps = {
   onNodeDoubleTap?: (e: GraphNodeDoubleTapEvent) => void;
   onEdgeTap?: (e: GraphEdgeTapEvent) => void;
   onDeleteTrafficRouting?: (key: string, serviceDetails: ServiceDetailsInfo) => void;
-  onLaunchWizard?: (key: WizardAction, mode: WizardMode, namespace: string, serviceDetails: ServiceDetailsInfo, gateways: string[], peerAuths: PeerAuthentication[]) => void;
+  onLaunchWizard?: (
+    key: WizardAction,
+    mode: WizardMode,
+    namespace: string,
+    serviceDetails: ServiceDetailsInfo,
+    gateways: string[],
+    peerAuths: PeerAuthentication[]
+  ) => void;
   onNodeTap?: (e: GraphNodeTapEvent) => void;
   onReady?: (cytoscapeRef: any) => void;
   rankBy: RankMode[];
diff --git a/frontend/src/components/CytoscapeGraph/MiniGraphCard.tsx b/frontend/src/components/CytoscapeGraph/MiniGraphCard.tsx
index ca9a39c16..0d8e4a8af 100644
--- a/frontend/src/components/CytoscapeGraph/MiniGraphCard.tsx
+++ b/frontend/src/components/CytoscapeGraph/MiniGraphCard.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { connect } from "react-redux";
+import { connect } from 'react-redux';
 import {
   Card,
   CardActions,
@@ -21,22 +21,22 @@ import { store } from 'store/ConfigStore';
 import { style } from 'typestyle';
 import { toRangeString } from '../Time/Utils';
 import { TimeInMilliseconds } from '../../types/Common';
-import { ServiceDetailsInfo } from "../../types/ServiceInfo";
+import { ServiceDetailsInfo } from '../../types/ServiceInfo';
 import { KialiDagreGraph } from './graphs/KialiDagreGraph';
-import {KialiAppState} from "../../store/Store";
-import { isParentKiosk, kioskContextMenuAction } from "../Kiosk/KioskActions";
-import { LoadingWizardActionsDropdownGroup } from "../IstioWizards/LoadingWizardActionsDropdownGroup";
-import ServiceWizardActionsDropdownGroup from "../IstioWizards/ServiceWizardActionsDropdownGroup";
-import { WizardAction, WizardMode } from "../IstioWizards/WizardActions";
-import { TimeDurationModal } from "../Time/TimeDurationModal";
-import TimeDurationIndicatorContainer from "../Time/TimeDurationIndicatorComponent";
-import { KioskElement } from "../Kiosk/KioskElement";
+import { KialiAppState } from '../../store/Store';
+import { isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions';
+import { LoadingWizardActionsDropdownGroup } from '../IstioWizards/LoadingWizardActionsDropdownGroup';
+import ServiceWizardActionsDropdownGroup from '../IstioWizards/ServiceWizardActionsDropdownGroup';
+import { WizardAction, WizardMode } from '../IstioWizards/WizardActions';
+import { TimeDurationModal } from '../Time/TimeDurationModal';
+import TimeDurationIndicatorContainer from '../Time/TimeDurationIndicatorComponent';
+import { KioskElement } from '../Kiosk/KioskElement';
 
 const initGraphContainerStyle = style({ width: '100%', height: '100%' });
 
 type ReduxProps = {
   kiosk: string;
-}
+};
 
 type MiniGraphCardProps = ReduxProps & {
   dataSource: GraphDataSource;
@@ -83,7 +83,7 @@ class MiniGraphCard extends React.Component<MiniGraphCardProps, MiniGraphCardSta
     const graphCardActions = [
       <DropdownItem key="viewFullGraph" onClick={this.onViewFullGraph}>
         Show full graph
-      </DropdownItem>,
+      </DropdownItem>
     ];
     if (isParentKiosk(this.props.kiosk)) {
       if (this.props.serviceDetails === undefined) {
@@ -96,7 +96,8 @@ class MiniGraphCard extends React.Component<MiniGraphCardProps, MiniGraphCardSta
             k8sHTTPRoutes={this.props.serviceDetails.k8sHTTPRoutes || []}
             istioPermissions={this.props.serviceDetails.istioPermissions}
             onAction={this.handleLaunchWizard}
-            onDelete={this.handleDeleteTrafficRouting} />
+            onDelete={this.handleDeleteTrafficRouting}
+          />
         );
       }
     } else {
@@ -107,7 +108,6 @@ class MiniGraphCard extends React.Component<MiniGraphCardProps, MiniGraphCardSta
       );
     }
 
-
     const rangeEnd: TimeInMilliseconds = this.props.dataSource.graphTimestamp * 1000;
     const rangeStart: TimeInMilliseconds = rangeEnd - this.props.dataSource.graphDuration * 1000;
     const intervalTitle =
@@ -181,7 +181,8 @@ class MiniGraphCard extends React.Component<MiniGraphCardProps, MiniGraphCardSta
           customDuration={false}
           isOpen={this.state.isTimeOptionsOpen}
           onConfirm={this.toggleTimeOptionsVisibility}
-          onCancel={this.toggleTimeOptionsVisibility} />
+          onCancel={this.toggleTimeOptionsVisibility}
+        />
       </>
     );
   }
@@ -195,14 +196,14 @@ class MiniGraphCard extends React.Component<MiniGraphCardProps, MiniGraphCardSta
     if (this.props.onLaunchWizard) {
       this.props.onLaunchWizard(key, mode);
     }
-  }
+  };
 
   private handleDeleteTrafficRouting = (key: string) => {
     this.onGraphActionsToggle(false);
     if (this.props.onDeleteTrafficRouting) {
       this.props.onDeleteTrafficRouting(key);
     }
-  }
+  };
 
   private handleNodeTap = (e: GraphNodeTapEvent) => {
     // Do nothing on inaccessible nodes or service entry nodes
@@ -233,7 +234,7 @@ class MiniGraphCard extends React.Component<MiniGraphCardProps, MiniGraphCardSta
     let resource = e[eNodeType];
     let resourceType: string = eNodeType === NodeType.APP ? 'application' : eNodeType;
 
-    const href= `/namespaces/${e.namespace}/${resourceType}s/${resource}`;
+    const href = `/namespaces/${e.namespace}/${resourceType}s/${resource}`;
     if (isParentKiosk(this.props.kiosk)) {
       kioskContextMenuAction(href);
     } else {
@@ -324,13 +325,13 @@ class MiniGraphCard extends React.Component<MiniGraphCardProps, MiniGraphCardSta
   };
 
   private toggleTimeOptionsVisibility = () => {
-    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }) );
-  }
+    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }));
+  };
 }
 
 const mapStateToProps = (state: KialiAppState): ReduxProps => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
 const MiniGraphCardContainer = connect(mapStateToProps)(MiniGraphCard);
-export default MiniGraphCardContainer
+export default MiniGraphCardContainer;
diff --git a/frontend/src/components/DebugInformation/DebugInformation.tsx b/frontend/src/components/DebugInformation/DebugInformation.tsx
index b7e41c50a..2e5158839 100644
--- a/frontend/src/components/DebugInformation/DebugInformation.tsx
+++ b/frontend/src/components/DebugInformation/DebugInformation.tsx
@@ -1,14 +1,9 @@
 import * as React from 'react';
 import { connect } from 'react-redux';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { style } from 'typestyle';
-import _ from 'lodash';
 import beautify from 'json-beautify';
-
-import authenticationConfig from '../../config/AuthenticationConfig';
 import { serverConfig } from '../../config';
 import { ComputedServerConfig } from '../../config/ServerConfig';
-import { AuthConfig } from '../../types/Auth';
 import { KialiAppState } from '../../store/Store';
 import {
   Alert,
@@ -17,8 +12,15 @@ import {
   Button,
   ButtonVariant,
   Modal,
-  ModalVariant
+  ModalVariant,
+  Tab
 } from '@patternfly/react-core';
+import { aceOptions } from '../../types/IstioConfigDetails';
+import AceEditor from 'react-ace';
+import ParameterizedTabs from '../Tab/Tabs';
+import { ICell, Table, TableBody, TableHeader, TableVariant } from '@patternfly/react-table';
+import { AuthConfig } from '../../types/Auth';
+import authenticationConfig from '../../config/AuthenticationConfig';
 
 enum CopyStatus {
   NOT_COPIED, // We haven't copied the current output
@@ -32,17 +34,12 @@ type DebugInformationProps = {
 };
 
 type DebugInformationState = {
-  show: boolean;
+  config: Object;
   copyStatus: CopyStatus;
+  currentTab: string;
+  show: boolean;
 };
 
-const textAreaStyle = style({
-  width: '100%',
-  height: '200px',
-  minHeight: '200px',
-  resize: 'vertical'
-});
-
 type DebugInformationData = {
   backendConfigs: {
     authenticationConfig: AuthConfig;
@@ -56,17 +53,60 @@ const copyToClipboardOptions = {
   message: 'We failed to automatically copy the text, please use: #{key}, Enter\t'
 };
 
+// Will be shown in Kiali Config and hidden in Additional state
+const propsToShow = [
+  'accessibleNamespaces',
+  'authStrategy',
+  'clusters',
+  'gatewayAPIEnabled',
+  'istioConfigMap',
+  'istioIdentityDomain',
+  'istioNamespace',
+  'istioStatusEnabled',
+  'logLevel',
+  'istioCanaryRevision',
+  'istioInjectionAction'
+];
+
+const propsToPatch = ['cyRef', 'summaryTarget', 'token', 'username'];
+
+const defaultTab = 'kialiConfig';
+
+const tabIndex: { [tab: string]: number } = {
+  kialiConfig: 0,
+  additionalState: 1
+};
+
 export class DebugInformation extends React.PureComponent<DebugInformationProps, DebugInformationState> {
-  private textareaRef;
+  aceEditorRef: React.RefObject<AceEditor>;
+  kialiConfig = {};
 
   constructor(props: DebugInformationProps) {
     super(props);
-    this.textareaRef = React.createRef();
-    this.state = { show: false, copyStatus: CopyStatus.NOT_COPIED };
+    this.aceEditorRef = React.createRef();
+
+    for (const key in serverConfig) {
+      if (propsToShow.includes(key)) {
+        if (typeof serverConfig[key] === 'string') {
+          this.kialiConfig[key] = serverConfig[key];
+        } else {
+          this.kialiConfig[key] = JSON.stringify(serverConfig[key]);
+        }
+      }
+    }
+    // Order config items
+    this.kialiConfig = Object.keys(this.kialiConfig)
+      .sort()
+      .reduce((obj, key) => {
+        obj[key] = this.kialiConfig[key];
+        return obj;
+      }, {});
+
+    this.state = { show: false, copyStatus: CopyStatus.NOT_COPIED, currentTab: defaultTab, config: this.kialiConfig };
   }
 
   open = () => {
-    this.setState({ show: true, copyStatus: CopyStatus.NOT_COPIED });
+    this.setState({ copyStatus: CopyStatus.NOT_COPIED, currentTab: defaultTab, show: true });
   };
 
   close = () => {
@@ -74,7 +114,6 @@ export class DebugInformation extends React.PureComponent<DebugInformationProps,
   };
 
   copyCallback = (_text: string, result: boolean) => {
-    this.textareaRef.current.select();
     this.setState({ copyStatus: result ? CopyStatus.COPIED : CopyStatus.NOT_COPIED });
   };
 
@@ -88,57 +127,99 @@ export class DebugInformation extends React.PureComponent<DebugInformationProps,
     }
   }
 
-  renderHealthConfig = (config: Array<any> | Object | RegExp | string) => {
-    if (Array.isArray(config)) {
-      let arr = [];
-      for (let v of config) {
-        arr.push(this.renderHealthConfig(v) as never);
-      }
-      return arr;
+  parseConfig = (key: string, value: any) => {
+    // We have to patch some runtime properties  we don't want to serialize
+    if (propsToPatch.includes(key)) {
+      return null;
     }
-    let result = {};
-    for (let [key, value] of Object.entries(config)) {
-      if ((value as Object).constructor.toString().includes('RegExp')) {
-        result[key] = (value as RegExp).toString();
-      } else if (typeof value !== 'object') {
-        result[key] = value;
-      } else {
-        result[key] = this.renderHealthConfig(value);
+    return value;
+  };
+
+  // Properties shown in Kiali Config are not shown again in Additional State
+  filterDebugInformation = (info: any) => {
+    if (info !== null) {
+      for (const [key] of Object.entries(info)) {
+        if (propsToShow.includes(key)) {
+          delete info[key];
+          continue;
+        }
       }
     }
-    return result;
+    return info;
   };
 
-  render() {
-    const parseConfig = (key: string, value: any) => {
-      // We have to patch some runtime properties  we don't want to serialize
-      if (['cyRef', 'summaryTarget', 'token', 'username'].includes(key)) {
-        return null;
-      }
-      if ('healthConfig' === key) {
-        return this.renderHealthConfig(value);
-      }
-      return value;
-    };
-    const renderHealthConfig = beautify(
-      {
-        healthConfig: serverConfig.healthConfig
+  renderDebugInformation() {
+    let debugInformation: DebugInformationData = {
+      backendConfigs: {
+        authenticationConfig: authenticationConfig,
+        computedServerConfig: serverConfig
       },
-      parseConfig,
-      2
+      currentURL: window.location.href,
+      reduxState: this.props.appState
+    };
+    debugInformation = this.filterDebugInformation(debugInformation);
+    return beautify(debugInformation, this.parseConfig, 2);
+  }
+
+  private columns = (): ICell[] => {
+    return [{ title: 'Configuration' }, { title: 'Value' }];
+  };
+
+  private getRows() {
+    var conf: string[][] = [];
+
+    for (const [k, v] of Object.entries(this.state.config)) {
+      if (typeof v !== 'string') {
+        conf.push([k, JSON.stringify(v)]);
+      } else {
+        conf.push([k, v]);
+      }
+    }
+    return conf;
+  }
+
+  private renderTabs() {
+    const kialiConfig = (
+      <Tab eventKey={0} title="Kiali Config" key="kialiConfig">
+        <span></span>
+
+        <CopyToClipboard onCopy={this.copyCallback} text={this.getRows()} options={copyToClipboardOptions}>
+          <Table header={<></>} variant={TableVariant.compact} cells={this.columns()} rows={this.getRows()}>
+            <TableHeader />
+            <TableBody />
+          </Table>
+        </CopyToClipboard>
+      </Tab>
+    );
+
+    const additionalState = (
+      <Tab eventKey={1} title="Additional State" key="additionalState">
+        <span>Please include this information when opening a bug:</span>
+        <CopyToClipboard
+          onCopy={this.copyCallback}
+          text={this.renderDebugInformation()}
+          options={copyToClipboardOptions}
+        >
+          <AceEditor
+            ref={this.aceEditorRef}
+            mode="yaml"
+            theme="eclipse"
+            width={'100%'}
+            className={'istio-ace-editor'}
+            wrapEnabled={true}
+            readOnly={true}
+            setOptions={aceOptions || { foldStyle: 'markbegin' }}
+            value={this.renderDebugInformation()}
+          />
+        </CopyToClipboard>
+      </Tab>
     );
-    const renderDebugInformation = _.memoize(() => {
-      const debugInformation: DebugInformationData = {
-        backendConfigs: {
-          authenticationConfig: authenticationConfig,
-          computedServerConfig: serverConfig
-        },
-        currentURL: window.location.href,
-        reduxState: this.props.appState
-      };
-      return beautify(debugInformation, parseConfig, 2);
-    });
 
+    const tabsArray: JSX.Element[] = [kialiConfig, additionalState];
+    return tabsArray;
+  }
+
+  render() {
     if (!this.state.show) {
       return null;
     }
@@ -151,7 +232,15 @@ export class DebugInformation extends React.PureComponent<DebugInformationProps,
         title="Debug information"
         actions={[
           <Button onClick={this.close}>Close</Button>,
-          <CopyToClipboard onCopy={this.copyCallback} text={renderDebugInformation()} options={copyToClipboardOptions}>
+          <CopyToClipboard
+            onCopy={this.copyCallback}
+            text={
+              this.state.currentTab === 'kialiConfig'
+                ? JSON.stringify(this.state.config, null, 2)
+                : this.renderDebugInformation()
+            }
+            options={copyToClipboardOptions}
+          >
             <Button variant={ButtonVariant.primary}>Copy</Button>
           </CopyToClipboard>
         ]}
@@ -174,12 +263,20 @@ export class DebugInformation extends React.PureComponent<DebugInformationProps,
             actionClose={<AlertActionCloseButton onClose={this.hideAlert} />}
           />
         )}
-        <span>Health Config</span>
-        <textarea className={textAreaStyle} readOnly={true} value={renderHealthConfig} />
-        <span>Please include this information when opening a bug.</span>
-        <CopyToClipboard onCopy={this.copyCallback} text={renderDebugInformation()} options={copyToClipboardOptions}>
-          <textarea ref={this.textareaRef} className={textAreaStyle} readOnly={true} value={renderDebugInformation()} />
-        </CopyToClipboard>
+        <ParameterizedTabs
+          id="basic-tabs"
+          onSelect={tabValue => {
+            this.setState({ currentTab: tabValue });
+            this.hideAlert();
+          }}
+          tabMap={tabIndex}
+          defaultTab={defaultTab}
+          activeTab={this.state.currentTab}
+          mountOnEnter={true}
+          unmountOnExit={true}
+        >
+          {this.renderTabs()}
+        </ParameterizedTabs>
       </Modal>
     );
   }
diff --git a/frontend/src/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx b/frontend/src/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx
index c1d83c604..90c820121 100644
--- a/frontend/src/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx
+++ b/frontend/src/components/DefaultSecondaryMasthead/DefaultSecondaryMasthead.tsx
@@ -2,10 +2,17 @@ import React from 'react';
 import { Title, TitleSizes } from '@patternfly/react-core';
 import NamespaceDropdownContainer from '../NamespaceDropdown';
 import { style } from 'typestyle';
+import { KialiIcon } from '../../config/KialiIcon';
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
 
 const titles = ['applications', 'istio', 'istio/new', 'mesh', 'services', 'workloads'];
 
-type Props = {
+type ReduxProps = {
+  istioAPIEnabled: boolean;
+};
+
+type Props = ReduxProps & {
   actionsToolbar?: JSX.Element;
   hideNamespaceSelector?: boolean;
   rightToolbar?: JSX.Element;
@@ -29,7 +36,7 @@ const actionsToolbarStyle = style({
   paddingTop: '17px'
 });
 
-export default class DefaultSecondaryMasthead extends React.Component<Props> {
+class DefaultSecondaryMastheadComponent extends React.Component<Props> {
   showTitle() {
     let path = window.location.pathname;
     path = path.substr(path.lastIndexOf('/console') + '/console'.length + 1);
@@ -47,9 +54,17 @@ export default class DefaultSecondaryMasthead extends React.Component<Props> {
       }
       return {
         title: (
-          <Title headingLevel="h1" size={TitleSizes['3xl']} style={{ margin: '15px 0 11px' }}>
-            {title}
-          </Title>
+          <>
+            <Title headingLevel="h1" size={TitleSizes['3xl']} style={{ margin: '15px 0 11px' }}>
+              {title}
+            </Title>
+            {!this.props.istioAPIEnabled && path.startsWith('istio/new/') && (
+              <div>
+                <KialiIcon.Warning /> <b>Istio API is disabled.</b> Be careful when creating the configuration as the
+                Istio config validations are disabled when the Istio API is disabled.
+              </div>
+            )}
+          </>
         ),
         disabled: disabled
       };
@@ -76,3 +91,10 @@ export default class DefaultSecondaryMasthead extends React.Component<Props> {
     );
   }
 }
+
+const mapStateToProps = (state: KialiAppState) => ({
+  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled
+});
+
+const DefaultSecondaryMasthead = connect(mapStateToProps)(DefaultSecondaryMastheadComponent);
+export default DefaultSecondaryMasthead;
diff --git a/frontend/src/components/Details/DetailDescription.tsx b/frontend/src/components/Details/DetailDescription.tsx
index f4e98802b..895ffcf0e 100644
--- a/frontend/src/components/Details/DetailDescription.tsx
+++ b/frontend/src/components/Details/DetailDescription.tsx
@@ -10,14 +10,14 @@ import { renderTrafficStatus } from '../Health/HealthDetails';
 import { createIcon } from '../Health/Helper';
 import { PFBadge, PFBadges } from '../Pf/PfBadges';
 import { KialiIcon } from '../../config/KialiIcon';
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
-import {isParentKiosk, kioskContextMenuAction} from "../Kiosk/KioskActions";
-import { isGateway } from "../../helpers/LabelFilterHelper";
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
+import { isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions';
+import { isGateway } from '../../helpers/LabelFilterHelper';
 
 type ReduxProps = {
   kiosk: string;
-}
+};
 
 type Props = ReduxProps & {
   namespace: string;
@@ -62,7 +62,9 @@ class DetailDescription extends React.Component<Props> {
         onClick={() => {
           kioskContextMenuAction(href);
         }}
-      >{appName}</Link>
+      >
+        {appName}
+      </Link>
     ) : (
       <Link to={href}>{appName}</Link>
     );
@@ -71,9 +73,7 @@ class DetailDescription extends React.Component<Props> {
         <div className={iconStyle}>
           <PFBadge badge={PFBadges.App} position={TooltipPosition.top} />
         </div>
-        <span>
-          {link}
-        </span>
+        <span>{link}</span>
       </li>
     );
   }
@@ -86,7 +86,9 @@ class DetailDescription extends React.Component<Props> {
         onClick={() => {
           kioskContextMenuAction(href);
         }}
-      >{serviceName}</Link>
+      >
+        {serviceName}
+      </Link>
     ) : (
       <Link to={href}>{serviceName}</Link>
     );
@@ -95,9 +97,7 @@ class DetailDescription extends React.Component<Props> {
         <div className={iconStyle}>
           <PFBadge badge={PFBadges.Service} position={TooltipPosition.top} />
         </div>
-        <span>
-          {link}
-        </span>
+        <span>{link}</span>
       </li>
     );
   }
@@ -136,9 +136,7 @@ class DetailDescription extends React.Component<Props> {
         {workload.workloadName}
       </Link>
     ) : (
-      <Link to={href}>
-        {workload.workloadName}
-      </Link>
+      <Link to={href}>{workload.workloadName}</Link>
     );
     return (
       <span key={'WorkloadItem_' + workload.workloadName}>
@@ -150,7 +148,13 @@ class DetailDescription extends React.Component<Props> {
           <KialiIcon.Info className={infoStyle} />
         </Tooltip>
         {!workload.istioSidecar && (
-          <MissingSidecar namespace={this.props.namespace} isGateway={isGateway(workload.labels)} tooltip={true} style={{ marginLeft: '10px' }} text={''} />
+          <MissingSidecar
+            namespace={this.props.namespace}
+            isGateway={isGateway(workload.labels)}
+            tooltip={true}
+            style={{ marginLeft: '10px' }}
+            text={''}
+          />
         )}
       </span>
     );
@@ -179,9 +183,7 @@ class DetailDescription extends React.Component<Props> {
           {workload.workloadName}
         </Link>
       ) : (
-        <Link to={href}>
-          {workload.workloadName}
-        </Link>
+        <Link to={href}>{workload.workloadName}</Link>
       );
       return (
         <span key={`WorkloadItem_${workload.workloadName}`}>
@@ -201,7 +203,13 @@ class DetailDescription extends React.Component<Props> {
             <span style={{ marginLeft: '10px' }}>{createIcon(sub.status)}</span>
           </Tooltip>
           {!workload.istioSidecar && (
-            <MissingSidecar namespace={this.props.namespace} isGateway={isGateway(workload.labels)} tooltip={true} style={{ marginLeft: '10px' }} text={''} />
+            <MissingSidecar
+              namespace={this.props.namespace}
+              isGateway={isGateway(workload.labels)}
+              tooltip={true}
+              style={{ marginLeft: '10px' }}
+              text={''}
+            />
           )}
         </span>
       );
@@ -304,8 +312,8 @@ class DetailDescription extends React.Component<Props> {
 }
 
 const mapStateToProps = (state: KialiAppState): ReduxProps => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
-const DetailDescriptionContainer = connect(mapStateToProps)(DetailDescription)
+const DetailDescriptionContainer = connect(mapStateToProps)(DetailDescription);
 export default DetailDescriptionContainer;
diff --git a/frontend/src/components/DurationDropdown/DurationDropdown.tsx b/frontend/src/components/DurationDropdown/DurationDropdown.tsx
index a468f9b4a..ac34d02b1 100644
--- a/frontend/src/components/DurationDropdown/DurationDropdown.tsx
+++ b/frontend/src/components/DurationDropdown/DurationDropdown.tsx
@@ -11,8 +11,8 @@ import { connect } from 'react-redux';
 import { HistoryManager, URLParam } from '../../app/History';
 import history from '../../app/History';
 import { TooltipPosition } from '@patternfly/react-core';
-import { isKioskMode } from "../../utils/SearchParamUtils";
-import { kioskDurationAction } from "../Kiosk/KioskActions";
+import { isKioskMode } from '../../utils/SearchParamUtils';
+import { kioskDurationAction } from '../Kiosk/KioskActions';
 
 type ReduxProps = {
   duration: DurationInSeconds;
@@ -31,7 +31,6 @@ type DurationDropdownProps = ReduxProps & {
 };
 
 export class DurationDropdown extends React.Component<DurationDropdownProps> {
-
   render() {
     const durations = humanDurations(serverConfig, this.props.prefix, this.props.suffix);
 
@@ -54,11 +53,10 @@ export class DurationDropdown extends React.Component<DurationDropdownProps> {
   private updateDurationInterval = (duration: number) => {
     this.props.setDuration(duration); // notify redux of the change
 
-    if (isKioskMode() ) {
+    if (isKioskMode()) {
       kioskDurationAction(duration);
     }
   };
-
 }
 
 const withDurations = DurationDropdownComponent => {
diff --git a/frontend/src/components/Envoy/EnvoyDetails.tsx b/frontend/src/components/Envoy/EnvoyDetails.tsx
index a03ef6a55..9803473b7 100644
--- a/frontend/src/components/Envoy/EnvoyDetails.tsx
+++ b/frontend/src/components/Envoy/EnvoyDetails.tsx
@@ -35,8 +35,11 @@ import CustomMetricsContainer from 'components/Metrics/CustomMetrics';
 import { serverConfig } from 'config';
 import { FilterSelected } from 'components/Filters/StatefulFilters';
 import history from '../../app/History';
-import {tabName as workloadTabName, defaultTab as workloadDefaultTab} from '../../pages/WorkloadDetails/WorkloadDetailsPage';
-import { TimeInMilliseconds } from "../../types/Common";
+import {
+  tabName as workloadTabName,
+  defaultTab as workloadDefaultTab
+} from '../../pages/WorkloadDetails/WorkloadDetailsPage';
+import { TimeInMilliseconds } from '../../types/Common';
 
 // Enables the search box for the ACEeditor
 require('ace-builds/src-noconflict/ext-searchbox');
@@ -49,8 +52,8 @@ const iconStyle = style({
 });
 
 const envoyTabs = ['clusters', 'listeners', 'routes', 'bootstrap', 'config', 'metrics'];
-const tabName = 'envoyTab'
-const defaultTab = 'clusters'
+const tabName = 'envoyTab';
+const defaultTab = 'clusters';
 
 export type ResourceSorts = { [resource: string]: ISortBy };
 
@@ -135,7 +138,7 @@ class EnvoyDetails extends React.Component<EnvoyDetailsProps, EnvoyDetailsState>
         resource: targetResource,
         activeKey: tabIndex
       });
-      const mainTab = new URLSearchParams(history.location.search).get(workloadTabName) || workloadDefaultTab
+      const mainTab = new URLSearchParams(history.location.search).get(workloadTabName) || workloadDefaultTab;
       const urlParams = new URLSearchParams('');
       urlParams.set(tabName, targetResource);
       urlParams.set(workloadTabName, mainTab);
@@ -272,8 +275,7 @@ class EnvoyDetails extends React.Component<EnvoyDetailsProps, EnvoyDetailsState>
     if (!envoyMetricsDashboardRef) {
       filteredEnvoyTabs = envoyTabs.slice(0, envoyTabs.length - 1);
     }
-    const tabs = filteredEnvoyTabs.map((value, index) =>
-    {
+    const tabs = filteredEnvoyTabs.map((value, index) => {
       const title = value.charAt(0).toUpperCase() + value.slice(1);
       return (
         <Tab style={{ backgroundColor: 'white' }} key={'tab_' + value} eventKey={index} title={title}>
@@ -343,9 +345,8 @@ class EnvoyDetails extends React.Component<EnvoyDetailsProps, EnvoyDetailsState>
             </CardBody>
           </Card>
         </Tab>
-      )
-    }
-    );
+      );
+    });
 
     return (
       <RenderComponentScroll onResize={height => this.setState({ tabHeight: height })}>
diff --git a/frontend/src/components/Envoy/tables/BaseTable.tsx b/frontend/src/components/Envoy/tables/BaseTable.tsx
index 715dc3769..ba3702f4a 100644
--- a/frontend/src/components/Envoy/tables/BaseTable.tsx
+++ b/frontend/src/components/Envoy/tables/BaseTable.tsx
@@ -73,9 +73,7 @@ export function SummaryTableRenderer<T extends SummaryTable>() {
                 label={this.props.pod}
                 options={this.props.pods.sort()}
               />
-              <div className={style({position: 'fixed', right: '60px'})}>
-               {this.props.writer.tooltip()}
-              </div>
+              <div className={style({ position: 'fixed', right: '60px' })}>{this.props.writer.tooltip()}</div>
             </>
           </StatefulFilters>
           <Table
@@ -102,7 +100,7 @@ export const SummaryTableBuilder = (
   namespace: string,
   routeLinkHandler: () => void,
   kiosk: string,
-  workload?: string,
+  workload?: string
 ) => {
   let writerComp, writerProps;
 
@@ -119,7 +117,7 @@ export const SummaryTableBuilder = (
         namespaces,
         namespace,
         workload,
-        routeLinkHandler,
+        routeLinkHandler
       );
       break;
     case 'routes':
diff --git a/frontend/src/components/Envoy/tables/ClusterTable.tsx b/frontend/src/components/Envoy/tables/ClusterTable.tsx
index 3c8626eaf..c0c29b38c 100644
--- a/frontend/src/components/Envoy/tables/ClusterTable.tsx
+++ b/frontend/src/components/Envoy/tables/ClusterTable.tsx
@@ -9,7 +9,7 @@ import { Tooltip } from '@patternfly/react-core';
 import { PFColors } from 'components/Pf/PfColors';
 import { KialiIcon } from 'config/KialiIcon';
 import { style } from 'typestyle';
-import {isParentKiosk} from "../../Kiosk/KioskActions";
+import { isParentKiosk } from '../../Kiosk/KioskActions';
 
 export class ClusterTable implements SummaryTable {
   summaries: ClusterSummary[];
@@ -66,18 +66,18 @@ export class ClusterTable implements SummaryTable {
 
   filterMethods = (): { [filter_id: string]: (ClusterSummary, ActiveFilter) => boolean } => {
     return {
-      "FQDN": (entry: ClusterSummary, filter: ActiveFilter): boolean => {
+      FQDN: (entry: ClusterSummary, filter: ActiveFilter): boolean => {
         return [entry.service_fqdn.service, entry.service_fqdn.namespace, entry.service_fqdn.cluster]
           .join('.')
           .includes(filter.value);
       },
-      "Port": (entry: ClusterSummary, filter: ActiveFilter): boolean => {
+      Port: (entry: ClusterSummary, filter: ActiveFilter): boolean => {
         return entry.port.toString().includes(filter.value);
       },
-      "Subset": (entry: ClusterSummary, filter: ActiveFilter): boolean => {
+      Subset: (entry: ClusterSummary, filter: ActiveFilter): boolean => {
         return entry.subset.toString().includes(filter.value);
       },
-      "Direction": (entry: ClusterSummary, filter: ActiveFilter): boolean => {
+      Direction: (entry: ClusterSummary, filter: ActiveFilter): boolean => {
         return entry.direction.toString().includes(filter.value);
       }
     };
@@ -146,28 +146,62 @@ export class ClusterTable implements SummaryTable {
 
   render_cluster_type = (): React.ReactNode => {
     return (
-        <ul className={style({textAlign: 'left'})}>
-          <li><b>STATIC</b>: Static is the simplest service discovery type. The configuration explicitly specifies the resolved network name (IP address/port, unix domain socket, etc.) of each upstream host.</li>
-          <li><b>STRICT_DNS</b>: Envoy will continuously and asynchronously resolve the specified DNS targets</li>
-          <li><b>LOGICAL_DNS</b>: Logical DNS uses a similar asynchronous resolution mechanism to strict DNS. However, instead of strictly taking the results of the DNS query and assuming that they comprise the entire upstream cluster, a logical DNS cluster only uses the first IP address returned when a new connection needs to be initiated</li>
-          <li><b>EDS</b>: The endpoint discovery service is a xDS management server based on gRPC or REST-JSON API server used by Envoy to fetch cluster members. </li>
-          <li><b>ORIGINAL_DST</b>: Original destination cluster can be used when incoming connections are redirected to Envoy either via an iptables REDIRECT or TPROXY target or with Proxy Protocol</li>
-        </ul>
-    )
-  }
+      <ul className={style({ textAlign: 'left' })}>
+        <li>
+          <b>STATIC</b>: Static is the simplest service discovery type. The configuration explicitly specifies the
+          resolved network name (IP address/port, unix domain socket, etc.) of each upstream host.
+        </li>
+        <li>
+          <b>STRICT_DNS</b>: Envoy will continuously and asynchronously resolve the specified DNS targets
+        </li>
+        <li>
+          <b>LOGICAL_DNS</b>: Logical DNS uses a similar asynchronous resolution mechanism to strict DNS. However,
+          instead of strictly taking the results of the DNS query and assuming that they comprise the entire upstream
+          cluster, a logical DNS cluster only uses the first IP address returned when a new connection needs to be
+          initiated
+        </li>
+        <li>
+          <b>EDS</b>: The endpoint discovery service is a xDS management server based on gRPC or REST-JSON API server
+          used by Envoy to fetch cluster members.{' '}
+        </li>
+        <li>
+          <b>ORIGINAL_DST</b>: Original destination cluster can be used when incoming connections are redirected to
+          Envoy either via an iptables REDIRECT or TPROXY target or with Proxy Protocol
+        </li>
+      </ul>
+    );
+  };
 
   head = (): ICell[] => {
     return [
-      { title: 'Service FQDN', transforms: [sortable], header: {info: { tooltip: <>Fully Qualified Domain Name</>}} },
+      {
+        title: 'Service FQDN',
+        transforms: [sortable],
+        header: { info: { tooltip: <>Fully Qualified Domain Name</> } }
+      },
       { title: 'Port', transforms: [sortable] },
       { title: 'Subset', transforms: [sortable] },
-      { title: 'Direction', transforms: [sortable], header: {info: { tooltip:
-      <ul className={style({textAlign: 'left'})}>
-        <li><b>inbound</b>: The inbound cluster events are the events that come into a node. These cluster events come from another node and enter other nodes.</li>
-        <li><b>outbound</b>: The outbound cluster events are the events that go out of a node. These cluster events are produced and sent from a node to other nodes.</li>
-      </ul>
-      }} },
-      { title: 'Type', transforms: [sortable], header: {info: { tooltip: this.render_cluster_type()}} },
+      {
+        title: 'Direction',
+        transforms: [sortable],
+        header: {
+          info: {
+            tooltip: (
+              <ul className={style({ textAlign: 'left' })}>
+                <li>
+                  <b>inbound</b>: The inbound cluster events are the events that come into a node. These cluster events
+                  come from another node and enter other nodes.
+                </li>
+                <li>
+                  <b>outbound</b>: The outbound cluster events are the events that go out of a node. These cluster
+                  events are produced and sent from a node to other nodes.
+                </li>
+              </ul>
+            )
+          }
+        }
+      },
+      { title: 'Type', transforms: [sortable], header: { info: { tooltip: this.render_cluster_type() } } },
       { title: 'DestinationRule', transforms: [sortable] }
     ];
   };
@@ -188,11 +222,17 @@ export class ClusterTable implements SummaryTable {
 
   tooltip = (): React.ReactNode => {
     return (
-      <Tooltip content={<div className={style({textAlign: 'left'})}>Group of logically similar upstream hosts that Envoy connects to. (All the hosts that envoy manage traffic)</div>}>
-          <KialiIcon.Help className={style({width: '14px', height: '14px', color: PFColors.Blue400})}/>
+      <Tooltip
+        content={
+          <div className={style({ textAlign: 'left' })}>
+            Group of logically similar upstream hosts that Envoy connects to. (All the hosts that envoy manage traffic)
+          </div>
+        }
+      >
+        <KialiIcon.Help className={style({ width: '14px', height: '14px', color: PFColors.Blue400 })} />
       </Tooltip>
     );
-  }
+  };
 
   rows(): (string | number | JSX.Element)[][] {
     const parentKiosk = isParentKiosk(this.kiosk);
diff --git a/frontend/src/components/Envoy/tables/ListenerTable.tsx b/frontend/src/components/Envoy/tables/ListenerTable.tsx
index 5594c4de1..4e1202796 100644
--- a/frontend/src/components/Envoy/tables/ListenerTable.tsx
+++ b/frontend/src/components/Envoy/tables/ListenerTable.tsx
@@ -10,7 +10,6 @@ import { PFColors } from 'components/Pf/PfColors';
 import { KialiIcon } from 'config/KialiIcon';
 import { style } from 'typestyle';
 
-
 export class ListenerTable implements SummaryTable {
   summaries: ListenerSummary[];
   sortingIndex: number;
@@ -26,7 +25,7 @@ export class ListenerTable implements SummaryTable {
     namespaces: Namespace[],
     namespace: string,
     workload: string | undefined,
-    routeLinkHandler: () => void,
+    routeLinkHandler: () => void
   ) {
     this.summaries = summaries;
     this.sortingIndex = sortBy.index || 0;
@@ -72,16 +71,16 @@ export class ListenerTable implements SummaryTable {
 
   filterMethods = (): { [filter_id: string]: (summary, activeFilter) => boolean } => {
     return {
-      "Address": (entry: ListenerSummary, filter: ActiveFilter): boolean => {
+      Address: (entry: ListenerSummary, filter: ActiveFilter): boolean => {
         return entry.address.includes(filter.value);
       },
-      "Port": (entry: ListenerSummary, filter: ActiveFilter): boolean => {
+      Port: (entry: ListenerSummary, filter: ActiveFilter): boolean => {
         return entry.port.toString().includes(filter.value);
       },
-      "Match": (entry: ListenerSummary, filter: ActiveFilter): boolean => {
+      Match: (entry: ListenerSummary, filter: ActiveFilter): boolean => {
         return entry.match.includes(filter.value);
       },
-      "Destination": (entry: ListenerSummary, filter: ActiveFilter): boolean => {
+      Destination: (entry: ListenerSummary, filter: ActiveFilter): boolean => {
         return entry.destination.includes(filter.value);
       }
     };
@@ -130,13 +129,37 @@ export class ListenerTable implements SummaryTable {
 
   head = (): ICell[] => {
     return [
-      { title: 'Address', transforms: [sortable], header: {info: { tooltip:
-        <div className={style({textAlign: 'left'})}>The address that the listener should listen on. In general, the address must be unique, though that is governed by the bind rules of the OS</div>}} },
+      {
+        title: 'Address',
+        transforms: [sortable],
+        header: {
+          info: {
+            tooltip: (
+              <div className={style({ textAlign: 'left' })}>
+                The address that the listener should listen on. In general, the address must be unique, though that is
+                governed by the bind rules of the OS
+              </div>
+            )
+          }
+        }
+      },
       { title: 'Port', transforms: [sortable] },
       { title: 'Match', transforms: [sortable] },
-      { title: 'Destination', transforms: [sortable], header: {info: { tooltip:
-        <div className={style({textAlign: 'left'})}>Original destination listener filter reads the SO_ORIGINAL_DST socket option set when a connection has been redirected by an iptables REDIRECT target, or by an iptables TPROXY target in combination with setting the listener’s transparent option</div>
-        }} }
+      {
+        title: 'Destination',
+        transforms: [sortable],
+        header: {
+          info: {
+            tooltip: (
+              <div className={style({ textAlign: 'left' })}>
+                Original destination listener filter reads the SO_ORIGINAL_DST socket option set when a connection has
+                been redirected by an iptables REDIRECT target, or by an iptables TPROXY target in combination with
+                setting the listener’s transparent option
+              </div>
+            )
+          }
+        }
+      }
     ];
   };
 
@@ -156,11 +179,18 @@ export class ListenerTable implements SummaryTable {
 
   tooltip = (): React.ReactNode => {
     return (
-      <Tooltip content={<div className={style({textAlign: 'left'})}>Network location that can be connected to by downstream clients (Incomming to envoy). List of endpoints:ports that envoy lets traffic</div>}>
-          <KialiIcon.Help className={style({width: '14px', height: '14px', color: PFColors.Blue400})}/>
+      <Tooltip
+        content={
+          <div className={style({ textAlign: 'left' })}>
+            Network location that can be connected to by downstream clients (Incomming to envoy). List of
+            endpoints:ports that envoy lets traffic
+          </div>
+        }
+      >
+        <KialiIcon.Help className={style({ width: '14px', height: '14px', color: PFColors.Blue400 })} />
       </Tooltip>
     );
-  }
+  };
 
   rows(): (string | number | JSX.Element)[][] {
     return this.summaries
diff --git a/frontend/src/components/Envoy/tables/RouteTable.tsx b/frontend/src/components/Envoy/tables/RouteTable.tsx
index d30c5bbba..b0abc13fd 100644
--- a/frontend/src/components/Envoy/tables/RouteTable.tsx
+++ b/frontend/src/components/Envoy/tables/RouteTable.tsx
@@ -9,7 +9,7 @@ import { Tooltip } from '@patternfly/react-core';
 import { PFColors } from 'components/Pf/PfColors';
 import { KialiIcon } from 'config/KialiIcon';
 import { style } from 'typestyle';
-import {isParentKiosk} from "../../Kiosk/KioskActions";
+import { isParentKiosk } from '../../Kiosk/KioskActions';
 
 export class RouteTable implements SummaryTable {
   summaries: RouteSummary[];
@@ -49,10 +49,10 @@ export class RouteTable implements SummaryTable {
 
   filterMethods = (): { [filter_id: string]: (ClusterSummary, ActiveFilter) => boolean } => {
     return {
-      "Name": (entry: RouteSummary, filter: ActiveFilter): boolean => {
+      Name: (entry: RouteSummary, filter: ActiveFilter): boolean => {
         return entry.name.toString().includes(filter.value);
       },
-      "Domains": (entry: RouteSummary, filter: ActiveFilter): boolean => {
+      Domains: (entry: RouteSummary, filter: ActiveFilter): boolean => {
         return [entry.domains.service, entry.domains.namespace, entry.domains.cluster].join('.').includes(filter.value);
       }
     };
@@ -104,12 +104,32 @@ export class RouteTable implements SummaryTable {
   head(): ICell[] {
     return [
       { title: 'Name', transforms: [sortable] },
-      { title: 'Domains', transforms: [sortable] , header: {info: { tooltip:
-        <div className={style({textAlign: 'left'})}>Envoy will be matched this domain to this virtual host.</div>
-        }}},
-      { title: 'Match', transforms: [sortable], header: {info: { tooltip:
-        <div className={style({textAlign: 'left'})}>The match tree to use when resolving route actions for incoming requests</div>
-        }} },
+      {
+        title: 'Domains',
+        transforms: [sortable],
+        header: {
+          info: {
+            tooltip: (
+              <div className={style({ textAlign: 'left' })}>
+                Envoy will be matched this domain to this virtual host.
+              </div>
+            )
+          }
+        }
+      },
+      {
+        title: 'Match',
+        transforms: [sortable],
+        header: {
+          info: {
+            tooltip: (
+              <div className={style({ textAlign: 'left' })}>
+                The match tree to use when resolving route actions for incoming requests
+              </div>
+            )
+          }
+        }
+      },
       { title: 'Virtual Service', transforms: [sortable] }
     ];
   }
@@ -130,11 +150,17 @@ export class RouteTable implements SummaryTable {
 
   tooltip = (): React.ReactNode => {
     return (
-      <Tooltip content={<div className={style({textAlign: 'left'})}>Network connection between source a destination that is configured in envoy</div>}>
-          <KialiIcon.Help className={style({width: '14px', height: '14px', color: PFColors.Blue400})}/>
+      <Tooltip
+        content={
+          <div className={style({ textAlign: 'left' })}>
+            Network connection between source a destination that is configured in envoy
+          </div>
+        }
+      >
+        <KialiIcon.Help className={style({ width: '14px', height: '14px', color: PFColors.Blue400 })} />
       </Tooltip>
     );
-  }
+  };
 
   rows(): (string | number | JSX.Element)[][] {
     const parentKiosk = isParentKiosk(this.kiosk);
diff --git a/frontend/src/components/ErrorSection/ErrorSection.tsx b/frontend/src/components/ErrorSection/ErrorSection.tsx
index ccae55dfa..fd1a948dd 100644
--- a/frontend/src/components/ErrorSection/ErrorSection.tsx
+++ b/frontend/src/components/ErrorSection/ErrorSection.tsx
@@ -1,28 +1,27 @@
 import * as React from 'react';
-import {style} from "typestyle";
-import {EmptyState, EmptyStateBody, EmptyStateVariant, Title, TitleSizes} from "@patternfly/react-core";
-import {ErrorMsg} from "../../types/ErrorMsg";
+import { style } from 'typestyle';
+import { EmptyState, EmptyStateBody, EmptyStateVariant, Title, TitleSizes } from '@patternfly/react-core';
+import { ErrorMsg } from '../../types/ErrorMsg';
 
 interface MessageProps {
   error: ErrorMsg;
 }
 
 const errorSectionStyle = style({
-  height: '80vh',
+  height: '80vh'
 });
 
 export default class ErrorSection extends React.Component<MessageProps> {
-
   render() {
     return (
       <div>
-      <EmptyState id="empty-page-error" variant={EmptyStateVariant.large} className={errorSectionStyle}>
-        <Title headingLevel="h5" size={TitleSizes.lg}>
-          {this.props.error.title}
-        </Title>
-        <EmptyStateBody>{this.props.error.description}</EmptyStateBody>
-      </EmptyState>
+        <EmptyState id="empty-page-error" variant={EmptyStateVariant.large} className={errorSectionStyle}>
+          <Title headingLevel="h5" size={TitleSizes.lg}>
+            {this.props.error.title}
+          </Title>
+          <EmptyStateBody>{this.props.error.description}</EmptyStateBody>
+        </EmptyState>
       </div>
-      );
+    );
   }
 }
diff --git a/frontend/src/components/HeatMap/HeatMap.tsx b/frontend/src/components/HeatMap/HeatMap.tsx
index 133bd29c4..eb37c5dda 100644
--- a/frontend/src/components/HeatMap/HeatMap.tsx
+++ b/frontend/src/components/HeatMap/HeatMap.tsx
@@ -97,9 +97,7 @@ export class HeatMap extends React.Component<Props> {
         {this.props.yLabels.map((yLabel, y) => {
           return (
             <React.Fragment key={`ylabel_${y}`}>
-              <div className={labelStyle}>
-                {isCompact ? '' : yLabel}
-              </div>
+              <div className={labelStyle}>{isCompact ? '' : yLabel}</div>
               {this.props.xLabels.map((_, x) => {
                 const value = this.props.data[x][y];
                 if (value) {
diff --git a/frontend/src/components/IstioActions/IstioActionsButtons.tsx b/frontend/src/components/IstioActions/IstioActionsButtons.tsx
index 7532f932e..1de9715df 100644
--- a/frontend/src/components/IstioActions/IstioActionsButtons.tsx
+++ b/frontend/src/components/IstioActions/IstioActionsButtons.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { Button, ButtonVariant } from '@patternfly/react-core';
-import { triggerRefresh } from "../../hooks/refresh";
+import { triggerRefresh } from '../../hooks/refresh';
 
 type Props = {
   objectName: string;
diff --git a/frontend/src/components/IstioActions/IstioActionsNamespaceDropdown.tsx b/frontend/src/components/IstioActions/IstioActionsNamespaceDropdown.tsx
index 0670796a7..a17df9d94 100644
--- a/frontend/src/components/IstioActions/IstioActionsNamespaceDropdown.tsx
+++ b/frontend/src/components/IstioActions/IstioActionsNamespaceDropdown.tsx
@@ -3,7 +3,7 @@ import { Dropdown, DropdownGroup, DropdownItem, DropdownPosition, DropdownToggle
 import history from '../../app/History';
 import { serverConfig } from '../../config';
 import { NEW_ISTIO_RESOURCE } from '../../pages/IstioConfigNew/IstioConfigNewPage';
-import {K8SGATEWAY} from "../../pages/IstioConfigNew/K8sGatewayForm";
+import { K8SGATEWAY } from '../../pages/IstioConfigNew/K8sGatewayForm';
 
 type Props = {};
 
@@ -45,7 +45,12 @@ class IstioActionsNamespaceDropdown extends React.Component<Props, State> {
       (r): ActionItem => ({
         name: r.value,
         action: (
-          <DropdownItem key={'createIstioConfig_' + r.value} isDisabled={r.value === K8SGATEWAY ? !serverConfig.gatewayAPIEnabled : r.disabled} onClick={() => this.onClickCreate(r.value)}>
+          <DropdownItem
+            key={'createIstioConfig_' + r.value}
+            isDisabled={r.value === K8SGATEWAY ? !serverConfig.gatewayAPIEnabled : r.disabled}
+            onClick={() => this.onClickCreate(r.value)}
+            data-test={'create_' + r.label}
+          >
             {r.label}
           </DropdownItem>
         )
@@ -64,7 +69,11 @@ class IstioActionsNamespaceDropdown extends React.Component<Props, State> {
       <Dropdown
         data-test="actions-dropdown"
         id="actions"
-        toggle={<DropdownToggle onToggle={this.onToggle}>Actions</DropdownToggle>}
+        toggle={
+          <DropdownToggle onToggle={this.onToggle} data-test="config-actions-dropdown">
+            Actions
+          </DropdownToggle>
+        }
         onSelect={this.onSelect}
         position={DropdownPosition.right}
         isOpen={this.state.dropdownOpen}
diff --git a/frontend/src/components/IstioCertsInfo/IstioCertsInfo.tsx b/frontend/src/components/IstioCertsInfo/IstioCertsInfo.tsx
index 83290de27..31ca15b3d 100644
--- a/frontend/src/components/IstioCertsInfo/IstioCertsInfo.tsx
+++ b/frontend/src/components/IstioCertsInfo/IstioCertsInfo.tsx
@@ -25,7 +25,7 @@ import { CertsInfo } from 'types/CertsInfo';
 import { PFColors } from 'components/Pf/PfColors';
 import { KialiIcon } from 'config/KialiIcon';
 import { infoStyle } from 'styles/DropdownStyles';
-import connectRefresh from "../Refresh/connectRefresh";
+import connectRefresh from '../Refresh/connectRefresh';
 
 type IstioCertsInfoState = {
   showModal: boolean;
@@ -171,15 +171,15 @@ class IstioCertsInfo extends React.Component<IstioCertsInfoProps, IstioCertsInfo
 }
 
 const mapStateToProps = (state: KialiAppState) => ({
-  certsInfo: istioCertsInfoSelector(state),
+  certsInfo: istioCertsInfoSelector(state)
 });
 
 const mapDispatchToProps = (dispatch: KialiDispatch) => ({
   setIstioCertsInfo: bindActionCreators(IstioCertsInfoActions.setinfo, dispatch)
 });
 
-const IstioCertsInfoConnected = connectRefresh(connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(
-  IstioCertsInfo
-));
+const IstioCertsInfoConnected = connectRefresh(
+  connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(IstioCertsInfo)
+);
 
 export default IstioCertsInfoConnected;
diff --git a/frontend/src/components/IstioConfigCard/IstioConfigCard.tsx b/frontend/src/components/IstioConfigCard/IstioConfigCard.tsx
index 3061923fb..91db91bec 100644
--- a/frontend/src/components/IstioConfigCard/IstioConfigCard.tsx
+++ b/frontend/src/components/IstioConfigCard/IstioConfigCard.tsx
@@ -16,12 +16,12 @@ import { ValidationObjectSummary } from '../Validations/ValidationObjectSummary'
 import { IstioTypes } from '../VirtualList/Config';
 import { style } from 'typestyle';
 import { PFBadge } from '../Pf/PfBadges';
-import IstioObjectLinkContainer from "../Link/IstioObjectLink";
+import IstioObjectLinkContainer from '../Link/IstioObjectLink';
 
 type Props = {
   name: string;
   items: IstioConfigItem[];
-}
+};
 
 const emtpytStyle = style({
   padding: '0 0 0 0',
@@ -40,7 +40,9 @@ export default class IstioConfigCard extends React.Component<Props> {
           {
             title: (
               <EmptyState variant={EmptyStateVariant.small} className={emtpytStyle}>
-                <EmptyStateBody className={emtpytStyle} data-test="istio-config-empty">No Istio Config found for {this.props.name}</EmptyStateBody>
+                <EmptyStateBody className={emtpytStyle} data-test="istio-config-empty">
+                  No Istio Config found for {this.props.name}
+                </EmptyStateBody>
               </EmptyState>
             ),
             props: { colSpan: 2 }
diff --git a/frontend/src/components/IstioConfigPreview/IstioConfigPreview.tsx b/frontend/src/components/IstioConfigPreview/IstioConfigPreview.tsx
index a98eb9221..fc822a586 100644
--- a/frontend/src/components/IstioConfigPreview/IstioConfigPreview.tsx
+++ b/frontend/src/components/IstioConfigPreview/IstioConfigPreview.tsx
@@ -136,12 +136,13 @@ export class IstioConfigPreview extends React.Component<Props, State> {
 
   addResource = (item: ConfigPreviewItem) => {
     const key = item.title.toLocaleLowerCase().replace(/\s/g, '');
-    const propItems =
+    const filterItems =
       this.props.items.length > 0
         ? (this.state.newIstioPage ? this.groupItems(this.props.items) : this.props.items).filter(
             it => it.title === item.title
-          )[0].items
+          )
         : [];
+    const propItems = filterItems.length > 0 ? filterItems[0].items : [];
     return (
       <Tab eventKey={key} key={key + '_tab_preview'} title={item.title}>
         <EditResources
diff --git a/frontend/src/components/IstioStatus/IstioStatus.tsx b/frontend/src/components/IstioStatus/IstioStatus.tsx
index 6d0aad6b2..543db5fad 100644
--- a/frontend/src/components/IstioStatus/IstioStatus.tsx
+++ b/frontend/src/components/IstioStatus/IstioStatus.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { SVGIconProps } from "@patternfly/react-icons/dist/esm/createIcon";
+import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
 import * as API from '../../services/Api';
 import * as AlertUtils from '../../utils/AlertUtils';
 import { TimeInMilliseconds } from '../../types/Common';
@@ -18,7 +18,7 @@ import './IstioStatus.css';
 import { ResourcesFullIcon } from '@patternfly/react-icons';
 import { KialiDispatch } from 'types/Redux';
 import NamespaceThunkActions from '../../actions/NamespaceThunkActions';
-import connectRefresh from "../Refresh/connectRefresh";
+import connectRefresh from '../Refresh/connectRefresh';
 
 type ReduxProps = {
   setIstioStatus: (istioStatus: ComponentStatus[]) => void;
@@ -28,15 +28,15 @@ type ReduxProps = {
 };
 
 type StatusIcons = {
-  ErrorIcon?: React.ComponentClass<SVGIconProps>,
-  WarningIcon?: React.ComponentClass<SVGIconProps>,
-  InfoIcon?: React.ComponentClass<SVGIconProps>,
-  HealthyIcon?: React.ComponentClass<SVGIconProps>
+  ErrorIcon?: React.ComponentClass<SVGIconProps>;
+  WarningIcon?: React.ComponentClass<SVGIconProps>;
+  InfoIcon?: React.ComponentClass<SVGIconProps>;
+  HealthyIcon?: React.ComponentClass<SVGIconProps>;
 };
 
 type Props = ReduxProps & {
   lastRefreshAt: TimeInMilliseconds;
-  icons?: StatusIcons
+  icons?: StatusIcons;
 };
 
 const ValidToColor = {
@@ -55,7 +55,7 @@ const defaultIcons = {
   WarningIcon: ResourcesFullIcon,
   InfoIcon: ResourcesFullIcon,
   HealthyIcon: ResourcesFullIcon
-}
+};
 
 export class IstioStatus extends React.Component<Props> {
   componentDidMount() {
diff --git a/frontend/src/components/IstioStatus/IstioStatusInline.tsx b/frontend/src/components/IstioStatus/IstioStatusInline.tsx
index ad754e3f5..c8a1002fc 100644
--- a/frontend/src/components/IstioStatus/IstioStatusInline.tsx
+++ b/frontend/src/components/IstioStatus/IstioStatusInline.tsx
@@ -1,17 +1,21 @@
 import * as React from 'react';
-import IstioStatus from "./IstioStatus";
+import IstioStatus from './IstioStatus';
 import {
   CheckCircleIcon,
   ExclamationCircleIcon,
   ExclamationTriangleIcon,
   MinusCircleIcon
-} from "@patternfly/react-icons";
+} from '@patternfly/react-icons';
 
 export default function IstioStatusInline() {
-  return <IstioStatus icons={{
-    ErrorIcon: ExclamationCircleIcon,
-    HealthyIcon: CheckCircleIcon,
-    InfoIcon: MinusCircleIcon,
-    WarningIcon: ExclamationTriangleIcon
-  }} />;
+  return (
+    <IstioStatus
+      icons={{
+        ErrorIcon: ExclamationCircleIcon,
+        HealthyIcon: CheckCircleIcon,
+        InfoIcon: MinusCircleIcon,
+        WarningIcon: ExclamationTriangleIcon
+      }}
+    />
+  );
 }
diff --git a/frontend/src/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx b/frontend/src/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx
index ee8bce8de..b658718f3 100644
--- a/frontend/src/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx
+++ b/frontend/src/components/IstioWizards/ConfirmDeleteTrafficRoutingModal.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import { Button, ButtonVariant, Modal, ModalVariant } from '@patternfly/react-core';
-import {DestinationRuleC, K8sHTTPRoute, VirtualService} from '../../types/IstioObjects';
+import { DestinationRuleC, K8sHTTPRoute, VirtualService } from '../../types/IstioObjects';
 
 type Props = {
   destinationRules: DestinationRuleC[];
@@ -48,8 +48,8 @@ const ConfirmDeleteTrafficRoutingModal: React.FunctionComponent<Props> = props =
     let k8sHTTPRouteMessage =
       props.k8sHTTPRoutes.length > 0
         ? `K8s HTTPRoute${props.k8sHTTPRoutes.length > 1 ? 's' : ''}: '${props.k8sHTTPRoutes.map(
-          k8sr => k8sr.metadata.name
-        )}'`
+            k8sr => k8sr.metadata.name
+          )}'`
         : '';
     deleteItems.push(<div key={`delete_item_${++i}`}>{k8sHTTPRouteMessage}</div>);
 
diff --git a/frontend/src/components/IstioWizards/K8sGatewaySelector.tsx b/frontend/src/components/IstioWizards/K8sGatewaySelector.tsx
index 29e044bb1..605a1376a 100644
--- a/frontend/src/components/IstioWizards/K8sGatewaySelector.tsx
+++ b/frontend/src/components/IstioWizards/K8sGatewaySelector.tsx
@@ -1,13 +1,5 @@
 import * as React from 'react';
-import {
-  Form,
-  FormGroup,
-  FormSelect,
-  FormSelectOption,
-  Radio,
-  Switch,
-  TextInput
-} from '@patternfly/react-core';
+import { Form, FormGroup, FormSelect, FormSelectOption, Radio, Switch, TextInput } from '@patternfly/react-core';
 import { GATEWAY_TOOLTIP, wizardTooltip } from './WizardHelp';
 import { isValid } from 'utils/Common';
 
@@ -47,7 +39,8 @@ class K8sGatewaySelector extends React.Component<Props, K8sGatewaySelectorState>
       gwHosts: props.k8sRouteHosts.join(','),
       gwHostsValid: true,
       newGateway: props.k8sGateways.length === 0,
-      selectedGateway: props.k8sGateways.length > 0 ? (props.gateway !== '' ? props.gateway : props.k8sGateways[0]) : '',
+      selectedGateway:
+        props.k8sGateways.length > 0 ? (props.gateway !== '' ? props.gateway : props.k8sGateways[0]) : '',
       addMesh: false,
       port: 80
     };
diff --git a/frontend/src/components/IstioWizards/K8sRequestRouting.tsx b/frontend/src/components/IstioWizards/K8sRequestRouting.tsx
index 17de8e580..413d9514e 100644
--- a/frontend/src/components/IstioWizards/K8sRequestRouting.tsx
+++ b/frontend/src/components/IstioWizards/K8sRequestRouting.tsx
@@ -1,12 +1,12 @@
 import * as React from 'react';
-import K8sRules, {MOVE_TYPE, K8sRule} from './K8sRequestRouting/K8sRules';
+import K8sRules, { MOVE_TYPE, K8sRule } from './K8sRequestRouting/K8sRules';
 import K8sRuleBuilder from './K8sRequestRouting/K8sRuleBuilder';
-import {K8sRouteBackendRef} from './K8sTrafficShifting';
+import { K8sRouteBackendRef } from './K8sTrafficShifting';
 import { EXACT, PATH, METHOD, GET, HEADERS, QUERY_PARAMS } from './K8sRequestRouting/K8sMatchBuilder';
-import {getDefaultBackendRefs} from './WizardActions';
-import {ServiceOverview} from "../../types/ServiceList";
-import {REMOVE, REQ_MOD, RESP_MOD, SET, HTTP, SC301, REQ_RED} from "./K8sRequestRouting/K8sFilterBuilder";
-import {isServerHostValid} from "../../utils/IstioConfigUtils";
+import { getDefaultBackendRefs, getDefaultService } from './WizardActions';
+import { ServiceOverview } from '../../types/ServiceList';
+import { REMOVE, REQ_MOD, RESP_MOD, SET, HTTP, SC301, REQ_RED, REQ_MIR } from './K8sRequestRouting/K8sFilterBuilder';
+import { isServerHostValid } from '../../utils/IstioConfigUtils';
 
 type Props = {
   subServices: ServiceOverview[];
@@ -32,6 +32,7 @@ type State = {
   queryParamName: string;
   schemeOp: string;
   statusCodeOp: string;
+  serviceOp: string;
   validationMsg: string;
 };
 
@@ -61,6 +62,7 @@ class K8sRequestRouting extends React.Component<Props, State> {
       headerOp: SET,
       schemeOp: HTTP,
       statusCodeOp: SC301,
+      serviceOp: getDefaultService(this.props.subServices),
       headerValue: '',
       hostName: '',
       portValue: '',
@@ -105,9 +107,11 @@ class K8sRequestRouting extends React.Component<Props, State> {
       if (prevState.category === PATH) {
         newMatch = prevState.category + ' ' + prevState.operator + ' ' + prevState.matchValue;
       } else if (prevState.category === HEADERS) {
-        newMatch = prevState.category + ' [' + prevState.headerName + '] ' + prevState.operator + ' ' + prevState.matchValue;
+        newMatch =
+          prevState.category + ' [' + prevState.headerName + '] ' + prevState.operator + ' ' + prevState.matchValue;
       } else if (prevState.category === QUERY_PARAMS) {
-        newMatch = prevState.category + ' ' + prevState.queryParamName + ' ' + prevState.operator + ' ' + prevState.matchValue;
+        newMatch =
+          prevState.category + ' ' + prevState.queryParamName + ' ' + prevState.operator + ' ' + prevState.matchValue;
       } else {
         newMatch = prevState.category + ' ' + prevState.operator;
       }
@@ -131,7 +135,7 @@ class K8sRequestRouting extends React.Component<Props, State> {
           newBackendRefs.push({
             name: br.name,
             weight: br.weight,
-            port: br.port ? br.port : 80,
+            port: br.port ? br.port : 80
           })
         );
         const newRule: K8sRule = {
@@ -215,7 +219,7 @@ class K8sRequestRouting extends React.Component<Props, State> {
   };
 
   onQueryParamNameChange = (queryParamName: string) => {
-      let validationMsg = '';
+    let validationMsg = '';
     if (this.state.matchValue !== '' && queryParamName === '') {
       validationMsg = MSG_QUERY_NAME_NON_EMPTY;
     }
@@ -310,6 +314,8 @@ class K8sRequestRouting extends React.Component<Props, State> {
         }
       } else if (this.state.filterType === REQ_RED) {
         newFilter = `${prevState.filterType} ${prevState.schemeOp}://${prevState.hostName}:${prevState.portValue} ${prevState.statusCodeOp}`;
+      } else if (this.state.filterType === REQ_MIR) {
+        newFilter = `${prevState.filterType} ${prevState.serviceOp}`;
       }
       if (newFilter && !prevState.filters.includes(newFilter)) {
         prevState.filters.push(newFilter);
@@ -336,18 +342,18 @@ class K8sRequestRouting extends React.Component<Props, State> {
       headerValue: headerValue,
       validationMsg: validationMsg
     });
-  }
+  };
 
   onHostNameChange = (hostName: string) => {
     let validationMsg = '';
-    if (!hostName || !isServerHostValid(hostName, false) ) {
+    if (!hostName || !isServerHostValid(hostName, false)) {
       validationMsg = MSG_HOSTNAME_NON_EMPTY;
     }
     this.setState({
       hostName: hostName,
       validationMsg: validationMsg
     });
-  }
+  };
 
   onPortValueChange = (portValue: string) => {
     let validationMsg = '';
@@ -358,7 +364,7 @@ class K8sRequestRouting extends React.Component<Props, State> {
       portValue: portValue,
       validationMsg: validationMsg
     });
-  }
+  };
 
   onRemoveFilter = (filterToRemove: string) => {
     this.setState(prevState => {
@@ -408,9 +414,11 @@ class K8sRequestRouting extends React.Component<Props, State> {
           onHostNameChange={this.onHostNameChange}
           onPortValueChange={this.onPortValueChange}
           onSelectStatusCodeOp={(statusCodeOp: string) => this.setState({ statusCodeOp: statusCodeOp })}
+          onSelectServiceOp={(serviceOp: string) => this.setState({ serviceOp: serviceOp })}
           headerOp={this.state.headerOp}
           schemeOp={this.state.schemeOp}
           statusCodeOp={this.state.statusCodeOp}
+          serviceOp={this.state.serviceOp}
           hostName={this.state.hostName}
           portValue={this.state.portValue}
           filterType={this.state.filterType}
diff --git a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilterBuilder.tsx b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilterBuilder.tsx
index f83618aba..d538444c1 100644
--- a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilterBuilder.tsx
+++ b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilterBuilder.tsx
@@ -8,6 +8,9 @@ import {
   TextInput,
   ButtonVariant
 } from '@patternfly/react-core';
+import { ServiceOverview } from '../../../types/ServiceList';
+import { getServicePort } from '../../../types/ServiceInfo';
+import { style } from 'typestyle';
 
 type Props = {
   filterType: string;
@@ -21,13 +24,16 @@ type Props = {
   onHeaderValueChange: (headerValue: string) => void;
   onHostNameChange: (hostName: string) => void;
   onPortValueChange: (portValue: string) => void;
+  onSelectServiceOp: (serviceOp: string) => void;
   onSelectStatusCodeOp: (statusCodeOp: string) => void;
   onSelectHeaderOp: (headerOp: string) => void;
   onSelectSchemeOp: (schemeOp: string) => void;
   onAddFilter: () => void;
   portValue: string;
   schemeOp: string;
+  serviceOp: string;
   statusCodeOp: string;
+  subServices: ServiceOverview[];
 };
 
 type State = {
@@ -35,6 +41,7 @@ type State = {
   isHeaderDropdown: boolean;
   isSchemeDropdown: boolean;
   isStatusCodeDropdown: boolean;
+  isServiceDropdown: boolean;
 };
 
 export const REQ_MOD = 'requestHeaderModifier';
@@ -50,7 +57,7 @@ export const HTTPS = 'https';
 export const SC301 = '301';
 export const SC302 = '302';
 
-const filterOptions: string[] = [REQ_MOD, REQ_RED];
+const filterOptions: string[] = [REQ_MOD, REQ_RED, REQ_MIR];
 const schemeOptions: string[] = [HTTP, HTTPS];
 const statusOptions: string[] = [SC301, SC302];
 
@@ -60,9 +67,13 @@ export const REMOVE = 'remove';
 
 const allOptions = {
   [REQ_MOD]: [SET, ADD, REMOVE],
-  [RESP_MOD]: [SET, ADD, REMOVE],
+  [RESP_MOD]: [SET, ADD, REMOVE]
 };
 
+const serviceStyle = style({
+  width: '100%'
+});
+
 class K8sFilterBuilder extends React.Component<Props, State> {
   constructor(props) {
     super(props);
@@ -71,6 +82,7 @@ class K8sFilterBuilder extends React.Component<Props, State> {
       isHeaderDropdown: false,
       isSchemeDropdown: false,
       isStatusCodeDropdown: false,
+      isServiceDropdown: false
     };
   }
 
@@ -98,8 +110,14 @@ class K8sFilterBuilder extends React.Component<Props, State> {
     });
   };
 
+  onServiceOpToggle = () => {
+    this.setState({
+      isServiceDropdown: !this.state.isServiceDropdown
+    });
+  };
+
   render() {
-    const renderFilterOptions: string[] = allOptions[this.props.filterType]
+    const renderFilterOptions: string[] = allOptions[this.props.filterType];
     return (
       <InputGroup>
         <Dropdown
@@ -150,21 +168,22 @@ class K8sFilterBuilder extends React.Component<Props, State> {
         )}
         {(this.props.filterType === REQ_MOD || this.props.filterType === RESP_MOD) && (
           <TextInput
-            id="header-name-id"
+            id="filter-header-name-id"
             value={this.props.headerName}
             onChange={this.props.onHeaderNameChange}
             placeholder="Header name..."
           />
         )}
-        {(this.props.filterType === REQ_MOD || this.props.filterType === RESP_MOD) && this.props.headerOp !== REMOVE && (
-          <TextInput
-            id="header-value-id"
-            value={this.props.headerValue}
-            onChange={this.props.onHeaderValueChange}
-            placeholder="Header Value..."
-          />
-        )}
-        {(this.props.filterType === REQ_RED) && (
+        {(this.props.filterType === REQ_MOD || this.props.filterType === RESP_MOD) &&
+          this.props.headerOp !== REMOVE && (
+            <TextInput
+              id="filter-header-value-id"
+              value={this.props.headerValue}
+              onChange={this.props.onHeaderValueChange}
+              placeholder="Header Value..."
+            />
+          )}
+        {this.props.filterType === REQ_RED && (
           <Dropdown
             toggle={
               <DropdownToggle onToggle={this.onSchemeOpToggle} data-test={'scheme-toggle'}>
@@ -188,7 +207,7 @@ class K8sFilterBuilder extends React.Component<Props, State> {
             ))}
           />
         )}
-        {(this.props.filterType === REQ_RED) && (
+        {this.props.filterType === REQ_RED && (
           <TextInput
             id="hostname"
             value={this.props.hostName}
@@ -196,7 +215,7 @@ class K8sFilterBuilder extends React.Component<Props, State> {
             placeholder="Hostname..."
           />
         )}
-        {(this.props.filterType === REQ_RED) && (
+        {this.props.filterType === REQ_RED && (
           <TextInput
             id="portValue"
             value={this.props.portValue}
@@ -204,7 +223,7 @@ class K8sFilterBuilder extends React.Component<Props, State> {
             placeholder="Port..."
           />
         )}
-        {(this.props.filterType === REQ_RED) && (
+        {this.props.filterType === REQ_RED && (
           <Dropdown
             toggle={
               <DropdownToggle onToggle={this.onStatusCodeOpToggle} data-test={'status-code'}>
@@ -228,11 +247,36 @@ class K8sFilterBuilder extends React.Component<Props, State> {
             ))}
           />
         )}
+        {this.props.filterType === REQ_MIR && (
+          <Dropdown
+            className={serviceStyle}
+            toggle={
+              <DropdownToggle onToggle={this.onServiceOpToggle} data-test={'service'}>
+                {this.props.serviceOp}
+              </DropdownToggle>
+            }
+            isOpen={this.state.isServiceDropdown}
+            dropdownItems={this.props.subServices.map((so, index) => (
+              <DropdownItem
+                key={so.name + '_' + index}
+                value={so.name + ':' + getServicePort(so.ports)}
+                component="button"
+                onClick={() => {
+                  this.props.onSelectServiceOp(so.name + ':' + getServicePort(so.ports));
+                  this.onServiceOpToggle();
+                }}
+                data-test={'service-' + so.name}
+              >
+                {so.name + ':' + getServicePort(so.ports)}
+              </DropdownItem>
+            ))}
+          />
+        )}
         <Button
           variant={ButtonVariant.secondary}
           isDisabled={!this.props.isValid}
           onClick={this.props.onAddFilter}
-          data-test="add-Filter"
+          data-test="add-filter"
         >
           Add Filter
         </Button>
diff --git a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilters.tsx b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilters.tsx
index b4091eb15..2b0838cc5 100644
--- a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilters.tsx
+++ b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sFilters.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react';
 import { Chip } from '@patternfly/react-core';
 import { style } from 'typestyle';
-import {FILTERING_SELECTED_TOOLTIP, wizardTooltip} from '../WizardHelp';
+import { FILTERING_SELECTED_TOOLTIP, wizardTooltip } from '../WizardHelp';
 
 type Props = {
   filters: string[];
@@ -14,7 +14,7 @@ const labelContainerStyle = style({
 });
 
 const remove = style({
-  cursor: "not-allowed"
+  cursor: 'not-allowed'
 });
 
 class K8sFilters extends React.Component<Props> {
diff --git a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatchBuilder.tsx b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatchBuilder.tsx
index 9a367c204..81f886d95 100644
--- a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatchBuilder.tsx
+++ b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatchBuilder.tsx
@@ -45,13 +45,13 @@ const allOptions = {
   [PATH]: [EXACT, PREFIX, REGEX],
   [HEADERS]: [EXACT, REGEX],
   [QUERY_PARAMS]: [EXACT, REGEX],
-  [METHOD]: ["CONNECT", "DELETE", GET, "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"]
+  [METHOD]: ['CONNECT', 'DELETE', GET, 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']
 };
 
 const placeholderText = {
   [PATH]: 'Path value...',
   [HEADERS]: 'Header value...',
-  [QUERY_PARAMS]: 'Query param value...',
+  [QUERY_PARAMS]: 'Query param value...'
 };
 
 class K8sMatchBuilder extends React.Component<Props, State> {
@@ -76,7 +76,7 @@ class K8sMatchBuilder extends React.Component<Props, State> {
   };
 
   render() {
-    const renderOpOptions: string[] = allOptions[this.props.category]
+    const renderOpOptions: string[] = allOptions[this.props.category];
     return (
       <InputGroup>
         <Dropdown
diff --git a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatches.tsx b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatches.tsx
index f0ca8a413..b39cf580e 100644
--- a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatches.tsx
+++ b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sMatches.tsx
@@ -14,7 +14,7 @@ const labelContainerStyle = style({
 });
 
 const remove = style({
-  cursor: "not-allowed"
+  cursor: 'not-allowed'
 });
 
 class K8sMatches extends React.Component<Props> {
diff --git a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRuleBuilder.tsx b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRuleBuilder.tsx
index bfd84e55f..4e38d9149 100644
--- a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRuleBuilder.tsx
+++ b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRuleBuilder.tsx
@@ -5,9 +5,9 @@ import K8sMatches from './K8sMatches';
 import { style } from 'typestyle';
 import { PFColors } from '../../Pf/PfColors';
 import K8sTrafficShifting, { K8sRouteBackendRef } from '../K8sTrafficShifting';
-import {ServiceOverview} from "../../../types/ServiceList";
-import K8sFilterBuilder from "./K8sFilterBuilder";
-import K8sFilters from "./K8sFilters";
+import { ServiceOverview } from '../../../types/ServiceList';
+import K8sFilterBuilder from './K8sFilterBuilder';
+import K8sFilters from './K8sFilters';
 
 type Props = {
   // K8sMatchBuilder props
@@ -37,12 +37,14 @@ type Props = {
   headerValue: string;
   hostName: string;
   portValue: string;
+  serviceOp: string;
   statusCodeOp: string;
   filters: string[];
   onSelectFilterType: (filterType: string) => void;
   onHeaderValueChange: (headerValue: string) => void;
   onHostNameChange: (hostName: string) => void;
   onPortValueChange: (portValue: string) => void;
+  onSelectServiceOp: (serviceOp: string) => void;
   onSelectStatusCodeOp: (statusCodeOp: string) => void;
   onSelectHeaderOp: (headerOp: string) => void;
   onSelectSchemeOp: (schemeOp: string) => void;
@@ -130,11 +132,7 @@ class K8sRuleBuilder extends React.Component<Props, State> {
         <div className={addRuleStyle}>
           <span>
             {this.props.validationMsg.length > 0 && <div className={validationStyle}>{this.props.validationMsg}</div>}
-            <Button
-              variant={ButtonVariant.secondary}
-              onClick={this.props.onAddRule}
-              data-test="add-route"
-            >
+            <Button variant={ButtonVariant.secondary} onClick={this.props.onAddRule} data-test="add-route">
               Add Route Rule
             </Button>
           </span>
diff --git a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRules.tsx b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRules.tsx
index 0ad259829..d9cb26a60 100644
--- a/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRules.tsx
+++ b/frontend/src/components/IstioWizards/K8sRequestRouting/K8sRules.tsx
@@ -12,7 +12,7 @@ import {
 } from '@patternfly/react-core';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { ROUTE_RULES_TOOLTIP, wizardTooltip } from '../WizardHelp';
-import {K8sRouteBackendRef} from '../K8sTrafficShifting';
+import { K8sRouteBackendRef } from '../K8sTrafficShifting';
 
 export enum MOVE_TYPE {
   UP,
@@ -138,8 +138,8 @@ class K8sRules extends React.Component<Props> {
                 </>,
                 <>
                   <div key={'br_' + order}>
-                    {rule.backendRefs && rule.backendRefs
-                      .map((bRef, i) => {
+                    {rule.backendRefs &&
+                      rule.backendRefs.map((bRef, i) => {
                         return (
                           <div key={'br_' + order + '_' + bRef.name + '_' + i}>
                             <PFBadge badge={PFBadges.Workload} position={TooltipPosition.top} />
diff --git a/frontend/src/components/IstioWizards/K8sRouteHosts.tsx b/frontend/src/components/IstioWizards/K8sRouteHosts.tsx
index b169f1b24..a3aea70dd 100644
--- a/frontend/src/components/IstioWizards/K8sRouteHosts.tsx
+++ b/frontend/src/components/IstioWizards/K8sRouteHosts.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
 import { Form, FormGroup, TextInput } from '@patternfly/react-core';
 import { K8sGatewaySelectorState } from './K8sGatewaySelector';
 import { isValid } from 'utils/Common';
-import {isGatewayHostValid} from "../../utils/IstioConfigUtils";
+import { isGatewayHostValid } from '../../utils/IstioConfigUtils';
 type Props = {
   k8sRouteHosts: string[];
   gateway?: K8sGatewaySelectorState;
@@ -16,7 +16,7 @@ class K8sRouteHosts extends React.Component<Props> {
         return false;
       }
       return true;
-    })
+    });
     return true;
   };
 
@@ -29,7 +29,7 @@ class K8sRouteHosts extends React.Component<Props> {
           fieldId="advanced-k8sRouteHosts"
           validated={isValid(this.isK8sRouteHostsValid(this.props.k8sRouteHosts))}
           helperText="The route hosts to which traffic is being sent. Enter one or multiple hosts separated by comma."
-          helperTextInvalid={"IPs are not allowed. A hostname may be prefixed with a wildcard label (*.)"}
+          helperTextInvalid={'IPs are not allowed. A hostname may be prefixed with a wildcard label (*.)'}
         >
           <TextInput
             value={k8sRouteHosts}
diff --git a/frontend/src/components/IstioWizards/K8sTrafficShifting.tsx b/frontend/src/components/IstioWizards/K8sTrafficShifting.tsx
index 090c1a5c4..c809e41c9 100644
--- a/frontend/src/components/IstioWizards/K8sTrafficShifting.tsx
+++ b/frontend/src/components/IstioWizards/K8sTrafficShifting.tsx
@@ -4,9 +4,9 @@ import Slider from './Slider/Slider';
 import { style } from 'typestyle';
 import { Button, ButtonVariant, TooltipPosition } from '@patternfly/react-core';
 import { EqualizerIcon } from '@patternfly/react-icons';
-import {getDefaultBackendRefs} from './WizardActions';
+import { getDefaultBackendRefs } from './WizardActions';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
-import {ServiceOverview} from "../../types/ServiceList";
+import { ServiceOverview } from '../../types/ServiceList';
 
 type Props = {
   subServices: ServiceOverview[];
@@ -78,7 +78,7 @@ class K8sTrafficShifting extends React.Component<Props, State> {
   onWeight = (serviceName: string, newWeight: number) => {
     this.setState(
       prevState => {
-         // Set new weight; remember rest of the nodes
+        // Set new weight; remember rest of the nodes
         for (let i = 0; i < prevState.backendRefs.length; i++) {
           if (prevState.backendRefs[i].name === serviceName) {
             prevState.backendRefs[i].weight = newWeight;
@@ -108,42 +108,41 @@ class K8sTrafficShifting extends React.Component<Props, State> {
         props: {}
       }
     ];
-    const servicesRows = this.state.backendRefs
-      .map(service => {
-        return {
-          cells: [
-            <>
-              <div>
-                <PFBadge badge={PFBadges.Workload} position={TooltipPosition.top} />
-                {service.name}
-              </div>
-            </>,
-            // This <> wrapper is needed by Slider
-            <>
-              <Slider
-                id={'slider-' + service.name}
-                key={'slider-' + service.name}
-                tooltip={true}
-                input={true}
-                inputFormat=""
-                value={service.weight}
-                min={0}
-                max={100}
-                maxLimit={100}
-                onSlide={value => {
-                  this.onWeight(service.name, value as number);
-                }}
-                onSlideStop={value => {
-                  this.onWeight(service.name, value as number);
-                }}
-                locked={false}
-                showLock={false}
-                mirrored={false}
-              />
-            </>
-          ]
-        };
-      });
+    const servicesRows = this.state.backendRefs.map(service => {
+      return {
+        cells: [
+          <>
+            <div>
+              <PFBadge badge={PFBadges.Workload} position={TooltipPosition.top} />
+              {service.name}
+            </div>
+          </>,
+          // This <> wrapper is needed by Slider
+          <>
+            <Slider
+              id={'slider-' + service.name}
+              key={'slider-' + service.name}
+              tooltip={true}
+              input={true}
+              inputFormat=""
+              value={service.weight}
+              min={0}
+              max={100}
+              maxLimit={100}
+              onSlide={value => {
+                this.onWeight(service.name, value as number);
+              }}
+              onSlideStop={value => {
+                this.onWeight(service.name, value as number);
+              }}
+              locked={false}
+              showLock={false}
+              mirrored={false}
+            />
+          </>
+        ]
+      };
+    });
     return (
       <>
         <Table cells={serviceCells} rows={servicesRows} aria-label="weighted routing">
diff --git a/frontend/src/components/IstioWizards/LoadingWizardActionsDropdownGroup.tsx b/frontend/src/components/IstioWizards/LoadingWizardActionsDropdownGroup.tsx
index a7b320996..cdc4c3fa1 100644
--- a/frontend/src/components/IstioWizards/LoadingWizardActionsDropdownGroup.tsx
+++ b/frontend/src/components/IstioWizards/LoadingWizardActionsDropdownGroup.tsx
@@ -1,5 +1,5 @@
-import { DropdownGroup, DropdownItem, Spinner } from "@patternfly/react-core";
-import * as React from "react";
+import { DropdownGroup, DropdownItem, Spinner } from '@patternfly/react-core';
+import * as React from 'react';
 
 export function LoadingWizardActionsDropdownGroup() {
   return (
diff --git a/frontend/src/components/IstioWizards/RequestRouting/Matches.tsx b/frontend/src/components/IstioWizards/RequestRouting/Matches.tsx
index 0d03149f7..af3451c41 100644
--- a/frontend/src/components/IstioWizards/RequestRouting/Matches.tsx
+++ b/frontend/src/components/IstioWizards/RequestRouting/Matches.tsx
@@ -14,7 +14,7 @@ const labelContainerStyle = style({
 });
 
 const remove = style({
-  cursor: "not-allowed"
+  cursor: 'not-allowed'
 });
 
 class Matches extends React.Component<Props> {
diff --git a/frontend/src/components/IstioWizards/ServiceWizard.tsx b/frontend/src/components/IstioWizards/ServiceWizard.tsx
index 130e99609..79ffb7ea5 100644
--- a/frontend/src/components/IstioWizards/ServiceWizard.tsx
+++ b/frontend/src/components/IstioWizards/ServiceWizard.tsx
@@ -1,21 +1,21 @@
 import * as React from 'react';
-import {Button, ButtonVariant, ExpandableSection, Modal, ModalVariant, Tab, Tabs} from '@patternfly/react-core';
-import {WorkloadOverview} from '../../types/ServiceInfo';
+import { Button, ButtonVariant, ExpandableSection, Modal, ModalVariant, Tab, Tabs } from '@patternfly/react-core';
+import { WorkloadOverview } from '../../types/ServiceInfo';
 import * as API from '../../services/Api';
-import {Response} from '../../services/Api';
+import { Response } from '../../services/Api';
 import * as AlertUtils from '../../utils/AlertUtils';
 import RequestRouting from './RequestRouting';
 import K8sRequestRouting from './K8sRequestRouting';
-import TrafficShifting, {WorkloadWeight} from './TrafficShifting';
+import TrafficShifting, { WorkloadWeight } from './TrafficShifting';
 import TrafficPolicyContainer, {
   ConsistentHashType,
   TrafficPolicyState,
   UNSET
 } from '../../components/IstioWizards/TrafficPolicy';
-import {ROUND_ROBIN} from './TrafficPolicy';
-import FaultInjection, {FaultInjectionRoute} from './FaultInjection';
-import {Rule} from './RequestRouting/Rules';
-import {K8sRule} from './K8sRequestRouting/K8sRules';
+import { ROUND_ROBIN } from './TrafficPolicy';
+import FaultInjection, { FaultInjectionRoute } from './FaultInjection';
+import { Rule } from './RequestRouting/Rules';
+import { K8sRule } from './K8sRequestRouting/K8sRules';
 import {
   buildIstioConfig,
   fqdnServiceName,
@@ -23,7 +23,8 @@ import {
   getInitFaultInjectionRoute,
   getInitGateway,
   getInitHosts,
-  getInitK8sGateway, getInitK8sHosts,
+  getInitK8sGateway,
+  getInitK8sHosts,
   getInitLoadBalancer,
   getInitOutlierDetection,
   getInitPeerAuthentication,
@@ -45,9 +46,9 @@ import {
   WIZARD_TRAFFIC_SHIFTING,
   WizardPreviews
 } from './WizardActions';
-import {MessageType} from '../../types/MessageCenter';
-import GatewaySelector, {GatewaySelectorState} from './GatewaySelector';
-import K8sGatewaySelector, {K8sGatewaySelectorState} from './K8sGatewaySelector';
+import { MessageType } from '../../types/MessageCenter';
+import GatewaySelector, { GatewaySelectorState } from './GatewaySelector';
+import K8sGatewaySelector, { K8sGatewaySelectorState } from './K8sGatewaySelector';
 import VirtualServiceHosts from './VirtualServiceHosts';
 import K8sRouteHosts from './K8sRouteHosts';
 import {
@@ -59,11 +60,12 @@ import {
   PeerAuthenticationMutualTLSMode,
   VirtualService
 } from '../../types/IstioObjects';
-import {style} from 'typestyle';
-import RequestTimeouts, {TimeoutRetryRoute} from './RequestTimeouts';
-import CircuitBreaker, {CircuitBreakerState} from './CircuitBreaker';
+import { style } from 'typestyle';
+import RequestTimeouts, { TimeoutRetryRoute } from './RequestTimeouts';
+import CircuitBreaker, { CircuitBreakerState } from './CircuitBreaker';
 import _ from 'lodash';
-import {ConfigPreviewItem, IstioConfigPreview} from 'components/IstioConfigPreview/IstioConfigPreview';
+import { ConfigPreviewItem, IstioConfigPreview } from 'components/IstioConfigPreview/IstioConfigPreview';
+import { KialiIcon } from '../../config/KialiIcon';
 
 const emptyServiceWizardState = (fqdnServiceName: string): ServiceWizardState => {
   return {
@@ -288,9 +290,10 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
           initVsHosts.length > 1 || (initVsHosts.length === 1 && initVsHosts[0].length > 0)
             ? initVsHosts
             : [fqdnServiceName(this.props.serviceName, this.props.namespace)],
-        k8sRouteHosts: initK8sRoutes.length > 1 || (initK8sRoutes.length === 1 && initK8sRoutes[0].length > 0)
-          ? initK8sRoutes
-          : [fqdnServiceName(this.props.serviceName, this.props.namespace)],
+        k8sRouteHosts:
+          initK8sRoutes.length > 1 || (initK8sRoutes.length === 1 && initK8sRoutes[0].length > 0)
+            ? initK8sRoutes
+            : [fqdnServiceName(this.props.serviceName, this.props.namespace)],
         trafficPolicy: trafficPolicy,
         gateway: gateway,
         k8sGateway: k8sGateway
@@ -341,7 +344,12 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
         if (this.props.update) {
           if (dr) {
             promises.push(
-              API.updateIstioConfigDetail(this.props.namespace, 'destinationrules', dr.metadata.name, JSON.stringify(dr))
+              API.updateIstioConfigDetail(
+                this.props.namespace,
+                'destinationrules',
+                dr.metadata.name,
+                JSON.stringify(dr)
+              )
             );
           }
           if (vs) {
@@ -351,7 +359,12 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
           }
           if (k8shttproute) {
             promises.push(
-              API.updateIstioConfigDetail(this.props.namespace, 'k8shttproutes', k8shttproute.metadata.name, JSON.stringify(k8shttproute))
+              API.updateIstioConfigDetail(
+                this.props.namespace,
+                'k8shttproutes',
+                k8shttproute.metadata.name,
+                JSON.stringify(k8shttproute)
+              )
             );
           }
 
@@ -367,7 +380,9 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
             promises.push(API.createIstioConfigDetail(this.props.namespace, 'virtualservices', JSON.stringify(vs)));
           }
           if (k8shttproute) {
-            promises.push(API.createIstioConfigDetail(this.props.namespace, 'k8shttproutes', JSON.stringify(k8shttproute)));
+            promises.push(
+              API.createIstioConfigDetail(this.props.namespace, 'k8shttproutes', JSON.stringify(k8shttproute))
+            );
           }
 
           if (pa) {
@@ -580,11 +595,7 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
   };
 
   isK8sAPIValid = (state: ServiceWizardState): boolean => {
-    return (
-      state.valid.mainWizard &&
-      state.valid.k8sRouteHosts &&
-      state.valid.gateway
-    );
+    return state.valid.mainWizard && state.valid.k8sRouteHosts && state.valid.gateway;
   };
 
   advancedHandleTabClick = (_event, tabIndex) => {
@@ -725,6 +736,12 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
               this.onConfirmPreview(items);
             }}
           />
+          {!this.props.istioAPIEnabled && (
+            <div style={{ padding: ' 0 0 20px 0' }}>
+              <KialiIcon.Warning /> <b>Istio API is disabled.</b> Be careful when editing the configuration as the Istio
+              config validations are disabled when the Istio API is disabled.
+            </div>
+          )}
           {this.props.type === WIZARD_REQUEST_ROUTING && (
             <RequestRouting
               serviceName={this.props.serviceName}
@@ -733,7 +750,7 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
               onChange={this.onRulesChange}
             />
           )}
-          {(this.props.type === WIZARD_K8S_REQUEST_ROUTING) && (
+          {this.props.type === WIZARD_K8S_REQUEST_ROUTING && (
             <K8sRequestRouting
               subServices={this.props.subServices}
               initRules={getInitK8sRules(this.props.k8sHTTPRoutes)}
@@ -848,7 +865,7 @@ class ServiceWizard extends React.Component<ServiceWizardProps, ServiceWizardSta
               </Tabs>
             </ExpandableSection>
           )}
-          {(this.props.type === WIZARD_K8S_REQUEST_ROUTING) && (
+          {this.props.type === WIZARD_K8S_REQUEST_ROUTING && (
             <ExpandableSection
               className={advancedOptionsStyle}
               isExpanded={this.state.showAdvanced}
diff --git a/frontend/src/components/IstioWizards/ServiceWizardActionsDropdownGroup.tsx b/frontend/src/components/IstioWizards/ServiceWizardActionsDropdownGroup.tsx
index c8b1fd30f..2e980df61 100644
--- a/frontend/src/components/IstioWizards/ServiceWizardActionsDropdownGroup.tsx
+++ b/frontend/src/components/IstioWizards/ServiceWizardActionsDropdownGroup.tsx
@@ -1,21 +1,16 @@
-import * as React from "react";
-import { DropdownGroup, DropdownItem, DropdownSeparator, Tooltip, TooltipPosition } from "@patternfly/react-core";
-import {serverConfig} from "config";
-import {
-  DestinationRule,
-  getWizardUpdateLabel,
-  K8sHTTPRoute,
-  VirtualService
-} from "types/IstioObjects";
-import { canDelete, ResourcePermissions } from "types/Permissions";
+import * as React from 'react';
+import { DropdownGroup, DropdownItem, DropdownSeparator, Tooltip, TooltipPosition } from '@patternfly/react-core';
+import { serverConfig } from 'config';
+import { DestinationRule, getWizardUpdateLabel, K8sHTTPRoute, VirtualService } from 'types/IstioObjects';
+import { canDelete, ResourcePermissions } from 'types/Permissions';
 import {
   SERVICE_WIZARD_ACTIONS,
   WIZARD_K8S_REQUEST_ROUTING,
   WIZARD_TITLES,
   WizardAction,
   WizardMode
-} from "./WizardActions";
-import { hasServiceDetailsTrafficRouting } from "../../types/ServiceInfo";
+} from './WizardActions';
+import { hasServiceDetailsTrafficRouting } from '../../types/ServiceInfo';
 
 export const DELETE_TRAFFIC_ROUTING = 'delete_traffic_routing';
 
@@ -27,7 +22,7 @@ type Props = {
   istioPermissions: ResourcePermissions;
   onAction?: (key: WizardAction, mode: WizardMode) => void;
   onDelete?: (key: string) => void;
-}
+};
 
 const ServiceWizardActionsDropdownGroup: React.FunctionComponent<Props> = props => {
   const updateLabel = getWizardUpdateLabel(props.virtualServices, props.k8sHTTPRoutes);
@@ -48,17 +43,26 @@ const ServiceWizardActionsDropdownGroup: React.FunctionComponent<Props> = props
     } else if (hasTrafficRouting()) {
       return 'Traffic routing already exists for this service';
     } else if (isGatewayAPI) {
-      return "K8s Gateway API is not enabled";
+      return 'K8s Gateway API is not enabled';
     } else {
       return "Traffic routing doesn't exists for this service";
     }
   }
 
   const actionItems = SERVICE_WIZARD_ACTIONS.map(eventKey => {
-    const isGatewayAPIEnabled = (eventKey === WIZARD_K8S_REQUEST_ROUTING ? serverConfig.gatewayAPIEnabled : true)
-    const enabledItem = isGatewayAPIEnabled && !props.isDisabled && (!hasTrafficRouting() || (hasTrafficRouting() && updateLabel === eventKey));
+    const isGatewayAPIEnabled = eventKey === WIZARD_K8S_REQUEST_ROUTING ? serverConfig.gatewayAPIEnabled : true;
+    const enabledItem =
+      isGatewayAPIEnabled &&
+      !props.isDisabled &&
+      (!hasTrafficRouting() || (hasTrafficRouting() && updateLabel === eventKey));
     const wizardItem = (
-      <DropdownItem key={eventKey} component="button" isDisabled={!enabledItem} onClick={() => handleActionClick(eventKey)} data-test={eventKey}>
+      <DropdownItem
+        key={eventKey}
+        component="button"
+        isDisabled={!enabledItem}
+        onClick={() => handleActionClick(eventKey)}
+        data-test={eventKey}
+      >
         {WIZARD_TITLES[eventKey]}
       </DropdownItem>
     );
@@ -69,10 +73,14 @@ const ServiceWizardActionsDropdownGroup: React.FunctionComponent<Props> = props
     // Otherwise, the item should be disabled
     if (!enabledItem) {
       return (
-        <Tooltip key={'tooltip_' + eventKey} position={TooltipPosition.left} content={<>{getDropdownItemTooltipMessage(!isGatewayAPIEnabled)}</>}>
+        <Tooltip
+          key={'tooltip_' + eventKey}
+          position={TooltipPosition.left}
+          content={<>{getDropdownItemTooltipMessage(!isGatewayAPIEnabled)}</>}
+        >
           <div style={{ display: 'inline-block', cursor: 'not-allowed' }}>{wizardItem}</div>
         </Tooltip>
-      )
+      );
     } else {
       return wizardItem;
     }
@@ -85,7 +93,11 @@ const ServiceWizardActionsDropdownGroup: React.FunctionComponent<Props> = props
     <DropdownItem
       key={DELETE_TRAFFIC_ROUTING}
       component="button"
-      onClick={() => {if (props.onDelete) { props.onDelete(DELETE_TRAFFIC_ROUTING); }}}
+      onClick={() => {
+        if (props.onDelete) {
+          props.onDelete(DELETE_TRAFFIC_ROUTING);
+        }
+      }}
       isDisabled={deleteDisabled}
       data-test={DELETE_TRAFFIC_ROUTING}
     >
@@ -95,7 +107,11 @@ const ServiceWizardActionsDropdownGroup: React.FunctionComponent<Props> = props
 
   if (deleteDisabled) {
     deleteDropdownItem = (
-      <Tooltip key={'tooltip_' + DELETE_TRAFFIC_ROUTING} position={TooltipPosition.left} content={<>{getDropdownItemTooltipMessage(false)}</>}>
+      <Tooltip
+        key={'tooltip_' + DELETE_TRAFFIC_ROUTING}
+        position={TooltipPosition.left}
+        content={<>{getDropdownItemTooltipMessage(false)}</>}
+      >
         <div style={{ display: 'inline-block', cursor: 'not-allowed' }}>{deleteDropdownItem}</div>
       </Tooltip>
     );
@@ -103,14 +119,7 @@ const ServiceWizardActionsDropdownGroup: React.FunctionComponent<Props> = props
 
   actionItems.push(deleteDropdownItem);
   const label = updateLabel === '' ? 'Create' : 'Update';
-  return (
-    <DropdownGroup
-      key={`group_${label}`}
-      label={label}
-      className="kiali-group-menu"
-      children={actionItems}
-    />
-  );
-}
+  return <DropdownGroup key={`group_${label}`} label={label} className="kiali-group-menu" children={actionItems} />;
+};
 
 export default ServiceWizardActionsDropdownGroup;
diff --git a/frontend/src/components/IstioWizards/ServiceWizardDropdown.tsx b/frontend/src/components/IstioWizards/ServiceWizardDropdown.tsx
index c39e8146f..9964ce348 100644
--- a/frontend/src/components/IstioWizards/ServiceWizardDropdown.tsx
+++ b/frontend/src/components/IstioWizards/ServiceWizardDropdown.tsx
@@ -1,11 +1,5 @@
 import * as React from 'react';
-import {
-  Dropdown,
-  DropdownPosition,
-  DropdownToggle,
-  Tooltip,
-  TooltipPosition
-} from '@patternfly/react-core';
+import { Dropdown, DropdownPosition, DropdownToggle, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { WorkloadOverview } from '../../types/ServiceInfo';
 import {
   DestinationRule,
@@ -28,12 +22,18 @@ import {
 } from './WizardActions';
 import ServiceWizard from './ServiceWizard';
 import { canCreate, canUpdate, ResourcePermissions } from '../../types/Permissions';
-import ServiceWizardActionsDropdownGroup, {DELETE_TRAFFIC_ROUTING} from "./ServiceWizardActionsDropdownGroup";
-import ConfirmDeleteTrafficRoutingModal from "./ConfirmDeleteTrafficRoutingModal";
-import { deleteServiceTrafficRouting } from "services/Api";
-import {ServiceOverview} from "../../types/ServiceList";
+import ServiceWizardActionsDropdownGroup, { DELETE_TRAFFIC_ROUTING } from './ServiceWizardActionsDropdownGroup';
+import ConfirmDeleteTrafficRoutingModal from './ConfirmDeleteTrafficRoutingModal';
+import { deleteServiceTrafficRouting } from 'services/Api';
+import { ServiceOverview } from '../../types/ServiceList';
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
+
+type ReduxProps = {
+  istioAPIEnabled: boolean;
+};
 
-type Props = {
+type Props = ReduxProps & {
   namespace: string;
   serviceName: string;
   show: boolean;
@@ -61,7 +61,7 @@ type State = {
   isActionsOpen: boolean;
 };
 
-class ServiceWizardDropdown extends React.Component<Props, State> {
+class ServiceWizardDropdownComponent extends React.Component<Props, State> {
   constructor(props: Props) {
     super(props);
     this.state = {
@@ -150,7 +150,11 @@ class ServiceWizardDropdown extends React.Component<Props, State> {
       isDeleting: true
     });
     this.hideConfirmDelete();
-    deleteServiceTrafficRouting(this.props.virtualServices, DestinationRuleC.fromDrArray(this.props.destinationRules), this.props.k8sHTTPRoutes)
+    deleteServiceTrafficRouting(
+      this.props.virtualServices,
+      DestinationRuleC.fromDrArray(this.props.destinationRules),
+      this.props.k8sHTTPRoutes
+    )
       .then(_results => {
         this.setState({
           isDeleting: false
@@ -233,6 +237,7 @@ class ServiceWizardDropdown extends React.Component<Props, State> {
           peerAuthentications={this.props.peerAuthentications}
           tlsStatus={this.props.tlsStatus}
           onClose={this.onClose}
+          istioAPIEnabled={this.props.istioAPIEnabled}
         />
         <ConfirmDeleteTrafficRoutingModal
           destinationRules={DestinationRuleC.fromDrArray(this.props.destinationRules)}
@@ -247,4 +252,9 @@ class ServiceWizardDropdown extends React.Component<Props, State> {
   }
 }
 
+const mapStateToProps = (state: KialiAppState) => ({
+  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled
+});
+
+const ServiceWizardDropdown = connect(mapStateToProps)(ServiceWizardDropdownComponent);
 export default ServiceWizardDropdown;
diff --git a/frontend/src/components/IstioWizards/WizardActions.ts b/frontend/src/components/IstioWizards/WizardActions.ts
index 688ed40ae..953886f21 100644
--- a/frontend/src/components/IstioWizards/WizardActions.ts
+++ b/frontend/src/components/IstioWizards/WizardActions.ts
@@ -1,5 +1,5 @@
 import { TLSStatus } from '../../types/TLSStatus';
-import {getServicePort, WorkloadOverview} from '../../types/ServiceInfo';
+import { getServicePort, WorkloadOverview } from '../../types/ServiceInfo';
 import { WorkloadWeight } from './TrafficShifting';
 import { Rule } from './RequestRouting/Rules';
 import { K8sRule } from './K8sRequestRouting/K8sRules';
@@ -15,8 +15,13 @@ import {
   HTTPMatchRequest,
   HTTPRoute,
   HTTPRouteDestination,
-  K8sGateway, K8sHTTPHeaderFilter,
-  K8sHTTPRoute, K8sHTTPRouteFilter, K8sHTTPRouteMatch, K8sHTTPRouteRequestRedirect,
+  K8sGateway,
+  K8sHTTPHeaderFilter,
+  K8sHTTPRequestMirrorFilter,
+  K8sHTTPRoute,
+  K8sHTTPRouteFilter,
+  K8sHTTPRouteMatch,
+  K8sHTTPRouteRequestRedirect,
   LoadBalancerSettings,
   Operation,
   OutlierDetection,
@@ -49,10 +54,10 @@ import { FaultInjectionRoute } from './FaultInjection';
 import { TimeoutRetryRoute } from './RequestTimeouts';
 import { DestService, GraphDefinition, NodeType } from '../../types/Graph';
 import { ServiceEntryState } from '../../pages/IstioConfigNew/ServiceEntryForm';
-import {K8sRouteBackendRef} from './K8sTrafficShifting';
-import { QUERY_PARAMS, PATH, HEADERS, METHOD } from "./K8sRequestRouting/K8sMatchBuilder";
-import {ServiceOverview} from "../../types/ServiceList";
-import {ADD, SET, REQ_MOD, REQ_RED} from "./K8sRequestRouting/K8sFilterBuilder";
+import { K8sRouteBackendRef } from './K8sTrafficShifting';
+import { QUERY_PARAMS, PATH, HEADERS, METHOD } from './K8sRequestRouting/K8sMatchBuilder';
+import { ServiceOverview } from '../../types/ServiceList';
+import { ADD, SET, REQ_MOD, REQ_RED, REQ_MIR } from './K8sRequestRouting/K8sFilterBuilder';
 
 export const WIZARD_TRAFFIC_SHIFTING = 'traffic_shifting';
 export const WIZARD_TCP_TRAFFIC_SHIFTING = 'tcp_traffic_shifting';
@@ -90,7 +95,7 @@ export const WIZARD_TITLES = {
   [WIZARD_TRAFFIC_SHIFTING]: 'Traffic Shifting',
   [WIZARD_TCP_TRAFFIC_SHIFTING]: 'TCP Traffic Shifting',
   [WIZARD_REQUEST_TIMEOUTS]: 'Request Timeouts',
-  [WIZARD_K8S_REQUEST_ROUTING]: 'K8s Gateway API Routing',
+  [WIZARD_K8S_REQUEST_ROUTING]: 'K8s Gateway API Routing'
 };
 
 export type ServiceWizardProps = {
@@ -108,9 +113,10 @@ export type ServiceWizardProps = {
   destinationRules: DestinationRule[];
   gateways: string[];
   k8sGateways: string[];
-  k8sHTTPRoutes: K8sHTTPRoute[],
+  k8sHTTPRoutes: K8sHTTPRoute[];
   peerAuthentications: PeerAuthentication[];
   onClose: (changed: boolean) => void;
+  istioAPIEnabled: boolean;
 };
 
 export type ServiceWizardValid = {
@@ -175,7 +181,7 @@ export const KIALI_RELATED_LABEL = 'kiali_wizard_related';
 // Wizard don't operate with EnvoyFilters so they can use the v1beta1 version
 export const ISTIO_NETWORKING_VERSION = 'networking.istio.io/v1beta1';
 export const ISTIO_SECURITY_VERSION = 'security.istio.io/v1beta1';
-export const GATEWAY_NETWORKING_VERSION = 'gateway.networking.k8s.io/v1alpha2';
+export const GATEWAY_NETWORKING_VERSION = 'gateway.networking.k8s.io/v1beta1';
 
 export const fqdnServiceName = (serviceName: string, namespace: string): string => {
   return serviceName + '.' + namespace + '.' + serverConfig.istioIdentityDomain;
@@ -225,7 +231,7 @@ const buildK8sHTTPRouteMatch = (matches: string[]): K8sHTTPRouteMatch => {
   const matchHeaders: HTTPMatch[] = [];
   const matchQueries: HTTPMatch[] = [];
   let matchPath = {};
-  let matchMethod = "";
+  let matchMethod = '';
 
   matches
     .filter(match => match.startsWith(HEADERS))
@@ -254,7 +260,7 @@ const buildK8sHTTPRouteMatch = (matches: string[]): K8sHTTPRouteMatch => {
       const op = match.substring(i + 1, j).trim();
       const value = match.substring(j + 1).trim();
       // Only one Path per Match
-      matchPath = {type: op, value: value}
+      matchPath = { type: op, value: value };
     });
   // QueryParams
   matches
@@ -267,7 +273,7 @@ const buildK8sHTTPRouteMatch = (matches: string[]): K8sHTTPRouteMatch => {
       const name = match.substring(i, j).trim();
       const op = match.substring(j + 1, k).trim();
       const value = match.substring(k + 1).trim();
-      matchQueries.push({name: name, type: op, value: value})
+      matchQueries.push({ name: name, type: op, value: value });
     });
   // Method
   matches
@@ -280,13 +286,13 @@ const buildK8sHTTPRouteMatch = (matches: string[]): K8sHTTPRouteMatch => {
       matchMethod = value;
     });
   if (matchHeaders.length > 0) {
-    matchRoute.headers = matchHeaders
+    matchRoute.headers = matchHeaders;
   }
   if (matchQueries.length > 0) {
-    matchRoute.queryParams = matchQueries
+    matchRoute.queryParams = matchQueries;
   }
   if (matchPath) {
-    matchRoute.path = matchPath
+    matchRoute.path = matchPath;
   }
   if (matchMethod) {
     matchRoute.method = matchMethod;
@@ -313,23 +319,23 @@ const buildK8sHTTPRouteFilter = (filters: string[]): K8sHTTPRouteFilter[] => {
       if (filterType === REQ_MOD && ((headerName && value) || removeHeader)) {
         if (op === ADD) {
           if (!requestHeaderModifier.add) {
-            requestHeaderModifier.add = []
+            requestHeaderModifier.add = [];
           }
-          requestHeaderModifier.add.push({name: headerName, value: value})
+          requestHeaderModifier.add.push({ name: headerName, value: value });
         } else if (op === SET) {
           if (!requestHeaderModifier.set) {
-            requestHeaderModifier.set = []
+            requestHeaderModifier.set = [];
           }
-          requestHeaderModifier.set.push({name: headerName, value: value})
+          requestHeaderModifier.set.push({ name: headerName, value: value });
         } else {
           if (!requestHeaderModifier.remove) {
-            requestHeaderModifier.remove = []
+            requestHeaderModifier.remove = [];
           }
-          requestHeaderModifier.remove.push(removeHeader)
+          requestHeaderModifier.remove.push(removeHeader);
         }
       }
 
-      routeFilter.push({type: "RequestHeaderModifier", requestHeaderModifier: requestHeaderModifier})
+      routeFilter.push({ type: 'RequestHeaderModifier', requestHeaderModifier: requestHeaderModifier });
     });
 
   filters
@@ -353,7 +359,22 @@ const buildK8sHTTPRouteFilter = (filters: string[]): K8sHTTPRouteFilter[] => {
         requestRedirect.port = parseInt(port);
       }
       requestRedirect.statusCode = code;
-      routeFilter.push({type: "RequestRedirect", requestRedirect: requestRedirect})
+      routeFilter.push({ type: 'RequestRedirect', requestRedirect: requestRedirect });
+    });
+
+  filters
+    .filter(filter => filter.startsWith(REQ_MIR))
+    .forEach(filter => {
+      const requestMirror: K8sHTTPRequestMirrorFilter = {};
+      // match follows format:  requestMirror service:port
+      const i0 = filter.indexOf(' ');
+      const j0 = filter.indexOf(':');
+      const service = filter.substring(i0 + 1, j0).trim();
+      const port = filter.substring(j0 + 1).trim();
+      if (service && port) {
+        requestMirror.backendRef = { name: service, port: parseInt(port) };
+      }
+      routeFilter.push({ type: 'RequestMirror', requestMirror: requestMirror });
     });
 
   return routeFilter;
@@ -410,7 +431,7 @@ const parseK8sHTTPMatchRequest = (httpRouteMatch: K8sHTTPRouteMatch): string[] =
   if (httpRouteMatch.queryParams) {
     httpRouteMatch.queryParams.forEach(qp => {
       matches.push('queryParam ' + qp.name + ' ' + qp.type + ' ' + qp.value);
-    })
+    });
   }
   if (httpRouteMatch.method) {
     matches.push('method ' + httpRouteMatch.method);
@@ -422,11 +443,14 @@ const parseK8sHTTPMatchRequest = (httpRouteMatch: K8sHTTPRouteMatch): string[] =
 const parseK8sHTTPRouteFilter = (httpRouteFilter: K8sHTTPRouteFilter): string[] => {
   let matches: string[] = [];
   if (httpRouteFilter.requestHeaderModifier) {
-    matches = matches.concat(parseK8sHTTPHeaderFilter("requestHeaderModifier", httpRouteFilter.requestHeaderModifier));
+    matches = matches.concat(parseK8sHTTPHeaderFilter('requestHeaderModifier', httpRouteFilter.requestHeaderModifier));
   }
   if (httpRouteFilter.requestRedirect) {
     matches = matches.concat(parseK8sHTTPRouteRequestRedirect(httpRouteFilter.requestRedirect));
   }
+  if (httpRouteFilter.requestMirror) {
+    matches = matches.concat(parseK8sHTTPRouteRequestMirror(httpRouteFilter.requestMirror));
+  }
   return matches;
 };
 
@@ -451,7 +475,15 @@ const parseK8sHTTPHeaderFilter = (filterType: string, httpHeaderFilter: K8sHTTPH
 };
 
 const parseK8sHTTPRouteRequestRedirect = (requestRedirect: K8sHTTPRouteRequestRedirect): string => {
-  return `${REQ_RED} ${requestRedirect.scheme}://${requestRedirect.hostname ? requestRedirect.hostname : ''}:${requestRedirect.port ? requestRedirect.port : ''} ${requestRedirect.statusCode}`;
+  return `${REQ_RED} ${requestRedirect.scheme}://${requestRedirect.hostname ? requestRedirect.hostname : ''}:${
+    requestRedirect.port ? requestRedirect.port : ''
+  } ${requestRedirect.statusCode}`;
+};
+
+const parseK8sHTTPRouteRequestMirror = (requestMirror: K8sHTTPRequestMirrorFilter): string => {
+  return `${REQ_MIR} ${
+    requestMirror.backendRef ? requestMirror.backendRef.name + ':' + requestMirror.backendRef.port : ''
+  }`;
 };
 
 export const getGatewayName = (namespace: string, serviceName: string, gatewayNames: string[]): string => {
@@ -489,10 +521,14 @@ export const buildIstioConfig = (wProps: ServiceWizardProps, wState: ServiceWiza
   let wizardK8sHTTPRoute: K8sHTTPRoute | undefined = undefined;
   let wizardDR: DestinationRule | undefined = undefined;
   let wizardVS: VirtualService | undefined = undefined;
-  let wizardGW: Gateway | undefined
-  let wizardK8sGW: K8sGateway | undefined
+  let wizardGW: Gateway | undefined;
+  let wizardK8sGW: K8sGateway | undefined;
   let wizardPA: PeerAuthentication | undefined = undefined;
-  const fullNewGatewayName = getGatewayName(wProps.namespace, wProps.serviceName, wProps.gateways.concat(wProps.k8sGateways));
+  const fullNewGatewayName = getGatewayName(
+    wProps.namespace,
+    wProps.serviceName,
+    wProps.gateways.concat(wProps.k8sGateways)
+  );
 
   if (wProps.type !== WIZARD_K8S_REQUEST_ROUTING) {
     wizardDR = {
@@ -556,31 +592,31 @@ export const buildIstioConfig = (wProps: ServiceWizardProps, wState: ServiceWiza
     wizardGW =
       wState.gateway && wState.gateway.addGateway && wState.gateway.newGateway
         ? {
-          kind: 'Gateway',
-          apiVersion: ISTIO_NETWORKING_VERSION,
-          metadata: {
-            namespace: wProps.namespace,
-            name: fullNewGatewayName.substr(wProps.namespace.length + 1),
-            labels: {
-              [KIALI_WIZARD_LABEL]: wProps.type
-            }
-          },
-          spec: {
-            selector: {
-              istio: 'ingressgateway'
-            },
-            servers: [
-              {
-                port: {
-                  number: wState.gateway.port,
-                  name: 'http',
-                  protocol: 'HTTP'
-                },
-                hosts: wState.gateway.gwHosts.split(',')
+            kind: 'Gateway',
+            apiVersion: ISTIO_NETWORKING_VERSION,
+            metadata: {
+              namespace: wProps.namespace,
+              name: fullNewGatewayName.substr(wProps.namespace.length + 1),
+              labels: {
+                [KIALI_WIZARD_LABEL]: wProps.type
               }
-            ]
+            },
+            spec: {
+              selector: {
+                istio: 'ingressgateway'
+              },
+              servers: [
+                {
+                  port: {
+                    number: wState.gateway.port,
+                    name: 'http',
+                    protocol: 'HTTP'
+                  },
+                  hosts: wState.gateway.gwHosts.split(',')
+                }
+              ]
+            }
           }
-        }
         : undefined;
 
     switch (wProps.type) {
@@ -793,7 +829,7 @@ export const buildIstioConfig = (wProps: ServiceWizardProps, wState: ServiceWiza
       const peerAuthnLabels: { [key: string]: string } = {};
       peerAuthnLabels[serverConfig.istioLabels.appLabelName] = wProps.workloads[0].labels![
         serverConfig.istioLabels.appLabelName
-        ];
+      ];
 
       wizardPA = {
         apiVersion: ISTIO_SECURITY_VERSION,
@@ -904,44 +940,48 @@ export const buildIstioConfig = (wProps: ServiceWizardProps, wState: ServiceWiza
   }
   if (wProps.type === WIZARD_K8S_REQUEST_ROUTING) {
     let k8sRouteName = wProps.serviceName;
-    if (wProps.k8sHTTPRoutes && wProps.k8sHTTPRoutes.length === 1 && wProps.k8sHTTPRoutes[0].metadata.name !== k8sRouteName) {
+    if (
+      wProps.k8sHTTPRoutes &&
+      wProps.k8sHTTPRoutes.length === 1 &&
+      wProps.k8sHTTPRoutes[0].metadata.name !== k8sRouteName
+    ) {
       k8sRouteName = wProps.k8sHTTPRoutes[0].metadata.name;
     }
 
     wizardK8sGW =
       wState.k8sGateway && wState.k8sGateway.addGateway && wState.k8sGateway.newGateway
         ? {
-          kind: 'Gateway',
-          apiVersion: GATEWAY_NETWORKING_VERSION,
-          metadata: {
-            namespace: wProps.namespace,
-            name: fullNewGatewayName.substr(wProps.namespace.length + 1),
-            labels: {
-              [KIALI_WIZARD_LABEL]: wProps.type,
-              app: fullNewGatewayName.substr(wProps.namespace.length + 1),
-            }
-          },
-          spec: {
-            gatewayClassName: 'istio',
-            listeners: [
-              {
-                name: 'default',
-                // here gwHosts for K8s API Gateway contains single host
-                hostname: wState.k8sGateway.gwHosts,
-                port: wState.k8sGateway.port,
-                protocol: 'HTTP',
-                allowedRoutes: {
-                  namespaces: {
-                    from: 'All',
-                    selector: {
-                      matchLabels: {}
+            kind: 'Gateway',
+            apiVersion: GATEWAY_NETWORKING_VERSION,
+            metadata: {
+              namespace: wProps.namespace,
+              name: fullNewGatewayName.substr(wProps.namespace.length + 1),
+              labels: {
+                [KIALI_WIZARD_LABEL]: wProps.type,
+                app: fullNewGatewayName.substr(wProps.namespace.length + 1)
+              }
+            },
+            spec: {
+              gatewayClassName: 'istio',
+              listeners: [
+                {
+                  name: 'default',
+                  // here gwHosts for K8s API Gateway contains single host
+                  hostname: wState.k8sGateway.gwHosts,
+                  port: wState.k8sGateway.port,
+                  protocol: 'HTTP',
+                  allowedRoutes: {
+                    namespaces: {
+                      from: 'All',
+                      selector: {
+                        matchLabels: {}
+                      }
                     }
                   }
                 }
-              }
-            ],
+              ]
+            }
           }
-        }
         : undefined;
 
     wizardK8sHTTPRoute = {
@@ -971,7 +1011,7 @@ export const buildIstioConfig = (wProps: ServiceWizardProps, wState: ServiceWiza
             const gwName = namespaceAndName[1];
             wizardK8sHTTPRoute.spec.parentRefs.push({
               name: gwName,
-              namespace: gwNamespace,
+              namespace: gwNamespace
             });
           } else if (wState.k8sGateway.selectedGateway.length > 0) {
             const namespaceAndName = wState.k8sGateway.selectedGateway.split('/');
@@ -979,7 +1019,8 @@ export const buildIstioConfig = (wProps: ServiceWizardProps, wState: ServiceWiza
             const gwName = namespaceAndName[1];
             wizardK8sHTTPRoute.spec.parentRefs.push({
               name: gwName,
-              namespace: gwNamespace});
+              namespace: gwNamespace
+            });
           }
         }
         if (wState.k8sRules && wState.k8sRules.length > 0) {
@@ -1002,7 +1043,14 @@ export const buildIstioConfig = (wProps: ServiceWizardProps, wState: ServiceWiza
       }
     }
   }
-  return { dr: wizardDR, vs: wizardVS, gw: wizardGW, k8sgateway: wizardK8sGW, pa: wizardPA, k8shttproute: wizardK8sHTTPRoute };
+  return {
+    dr: wizardDR,
+    vs: wizardVS,
+    gw: wizardGW,
+    k8sgateway: wizardK8sGW,
+    pa: wizardPA,
+    k8shttproute: wizardK8sHTTPRoute
+  };
 };
 
 const getWorkloadsByVersion = (
@@ -1032,11 +1080,20 @@ export const getDefaultBackendRefs = (subServices: ServiceOverview[]): K8sRouteB
     port: getServicePort(s.ports)
   }));
   if (remainTraffic > 0) {
-    backendRefs[backendRefs.length - 1].weight = backendRefs[backendRefs.length - 1].weight ? backendRefs[backendRefs.length - 1].weight : 0 + remainTraffic;
+    backendRefs[backendRefs.length - 1].weight = backendRefs[backendRefs.length - 1].weight
+      ? backendRefs[backendRefs.length - 1].weight
+      : 0 + remainTraffic;
   }
   return backendRefs;
 };
 
+export const getDefaultService = (subServices: ServiceOverview[]): string => {
+  if (subServices && subServices.length > 0) {
+    return `${subServices[0].name}:${getServicePort(subServices[0].ports)}`;
+  }
+  return '';
+};
+
 export const getDefaultWeights = (workloads: WorkloadOverview[]): WorkloadWeight[] => {
   const wkTraffic = workloads.length < 100 ? Math.floor(100 / workloads.length) : 0;
   const remainTraffic = workloads.length < 100 ? 100 % workloads.length : 0;
@@ -1194,9 +1251,7 @@ export const getInitRules = (
   return rules;
 };
 
-export const getInitK8sRules = (
-  httpRoutes: K8sHTTPRoute[]
-): K8sRule[] => {
+export const getInitK8sRules = (httpRoutes: K8sHTTPRoute[]): K8sRule[] => {
   const rules: K8sRule[] = [];
   if (httpRoutes && httpRoutes.length === 1 && httpRoutes[0].spec.rules) {
     httpRoutes[0].spec.rules.forEach(httpRoute => {
@@ -1477,9 +1532,9 @@ export const getInitK8sGateway = (k8sHTTPRoutes: K8sHTTPRoute[]): string => {
     k8sHTTPRoutes[0].spec.parentRefs &&
     k8sHTTPRoutes[0].spec.parentRefs.length > 0
   ) {
-    const name = k8sHTTPRoutes[0].spec.parentRefs[0].name
-    const namespace = k8sHTTPRoutes[0].spec.parentRefs[0].namespace
-    return (namespace !== '' ? namespace+'/' : '') + name;
+    const name = k8sHTTPRoutes[0].spec.parentRefs[0].name;
+    const namespace = k8sHTTPRoutes[0].spec.parentRefs[0].namespace;
+    return (namespace !== '' ? namespace + '/' : '') + name;
   }
   return '';
 };
@@ -1797,27 +1852,15 @@ export const buildK8sGateway = (name: string, namespace: string, state: K8sGatew
             selector: {
               matchLabels: s.allowedRoutes.namespaces.selector?.matchLabels
             }
-          },
+          }
         }
       })),
       addresses: state.addresses.map(s => ({
         type: s.type,
         value: s.value
-      })),
+      }))
     }
   };
-  state.workloadSelectorLabels
-    .trim()
-    .split(',')
-    .forEach(split => {
-      const labels = split.trim().split('=');
-      // It should be already validated with workloadSelectorValid, but just to add extra safe check
-      if (labels.length === 2) {
-        if (k8sGateway.metadata.labels) {
-          k8sGateway.metadata.labels[labels[0].trim()] = labels[1].trim();
-        }
-      }
-    });
   return k8sGateway;
 };
 
@@ -1986,18 +2029,24 @@ export const buildWorkloadInjectionPatch = (workloadType: string, enable: boolea
   const patch = {};
 
   // environments prefer to use the pod label over the annotation
+  // we will be opinionated here and always use the label and remove any annotation that may exist to avoid conflicts
   const labels = {};
   labels[serverConfig.istioAnnotations.istioInjectionAnnotation] = remove ? null : enable ? 'true' : 'false';
+  const annotations = {};
+  annotations[serverConfig.istioAnnotations.istioInjectionAnnotation] = null;
   if (workloadType === 'Pod') {
     patch['labels'] = labels;
+    patch['annotations'] = annotations;
   } else {
     patch['spec'] = {
       template: {
         metadata: {
-          labels: labels
+          labels: labels,
+          annotations: annotations
         }
       }
     };
   }
+
   return JSON.stringify(patch);
 };
diff --git a/frontend/src/components/IstioWizards/WorkloadWizardDropdown.tsx b/frontend/src/components/IstioWizards/WorkloadWizardDropdown.tsx
index f43516486..6abe9edeb 100644
--- a/frontend/src/components/IstioWizards/WorkloadWizardDropdown.tsx
+++ b/frontend/src/components/IstioWizards/WorkloadWizardDropdown.tsx
@@ -68,11 +68,7 @@ class WorkloadWizardDropdown extends React.Component<Props, State> {
       case WIZARD_REMOVE_AUTO_INJECTION:
         const remove = key === WIZARD_REMOVE_AUTO_INJECTION;
         const enable = key === WIZARD_ENABLE_AUTO_INJECTION;
-        const jsonInjectionPatch = buildWorkloadInjectionPatch(
-          this.props.workload.type,
-          enable,
-          remove
-        );
+        const jsonInjectionPatch = buildWorkloadInjectionPatch(this.props.workload.type, enable, remove);
         API.updateWorkload(this.props.namespace, this.props.workload.name, this.props.workload.type, jsonInjectionPatch)
           .then(_ => {
             AlertUtils.add('Workload ' + this.props.workload.name + ' updated', 'default', MessageType.SUCCESS);
@@ -201,11 +197,7 @@ class WorkloadWizardDropdown extends React.Component<Props, State> {
         data-test="workload-actions-dropdown"
         position={DropdownPosition.right}
         onSelect={this.onActionsSelect}
-        toggle={
-          <DropdownToggle onToggle={this.onActionsToggle}>
-            Actions
-          </DropdownToggle>
-        }
+        toggle={<DropdownToggle onToggle={this.onActionsToggle}>Actions</DropdownToggle>}
         isOpen={this.state.isActionsOpen}
         dropdownItems={this.renderDropdownItems()}
         disabled={!validActions}
diff --git a/frontend/src/components/JaegerIntegration/JaegerResults/TraceLabels.tsx b/frontend/src/components/JaegerIntegration/JaegerResults/TraceLabels.tsx
index 93263f742..65f593c14 100644
--- a/frontend/src/components/JaegerIntegration/JaegerResults/TraceLabels.tsx
+++ b/frontend/src/components/JaegerIntegration/JaegerResults/TraceLabels.tsx
@@ -36,9 +36,14 @@ export const TraceLabels = (p: Props) => {
       </Label>
       {!p.oneline && <br />}
       {errors === 0 ? (
-        <Label className="whiteColorLabelContent" style={{ margin: 10, backgroundColor: PFColors.Success }}>0 Spans with error</Label>
+        <Label className="whiteColorLabelContent" style={{ margin: 10, backgroundColor: PFColors.Success }}>
+          0 Spans with error
+        </Label>
       ) : (
-        <Label className="whiteColorLabelContent" style={{ margin: 10, backgroundColor: filteredErrors === 0 ? PFColors.Warning : PFColors.Danger }}>
+        <Label
+          className="whiteColorLabelContent"
+          style={{ margin: 10, backgroundColor: filteredErrors === 0 ? PFColors.Warning : PFColors.Danger }}
+        >
           {p.filteredSpans && `${filteredErrors} / `}
           {pluralize(errors, 'Span')} with error
         </Label>
diff --git a/frontend/src/components/JaegerIntegration/TracesDisplayOptions.tsx b/frontend/src/components/JaegerIntegration/TracesDisplayOptions.tsx
index 61f612a5a..0abe1c0f3 100644
--- a/frontend/src/components/JaegerIntegration/TracesDisplayOptions.tsx
+++ b/frontend/src/components/JaegerIntegration/TracesDisplayOptions.tsx
@@ -1,9 +1,8 @@
 import * as React from 'react';
-import { Checkbox, Dropdown, DropdownToggle, Radio, Tooltip } from '@patternfly/react-core';
-import { InfoAltIcon } from '@patternfly/react-icons';
-
-import { itemStyleWithoutInfo, menuStyle, titleStyle } from 'styles/DropdownStyles';
+import { Checkbox, Dropdown, DropdownToggle, Radio, Tooltip, TooltipPosition } from '@patternfly/react-core';
+import { infoStyle, itemStyleWithoutInfo, menuStyle, titleStyle } from 'styles/DropdownStyles';
 import { HistoryManager, URLParam } from 'app/History';
+import { KialiIcon } from 'config/KialiIcon';
 
 export interface QuerySettings {
   percentile?: string;
@@ -103,17 +102,26 @@ export class TracesDisplayOptions extends React.Component<Props, State> {
   private getPopoverContent() {
     return (
       <div id="traces-display-menu" className={menuStyle}>
-        <Tooltip
-          content={`
-          These percentiles are computed from metrics. To refresh them, reload the page.
-          The filter applies on span durations.
-          Thus, the filtered traces are the ones where at least one span for the service satisfies the duration criteria.
-        `}
-        >
-          <div className={titleStyle}>
-            Filter by percentile <InfoAltIcon />
-          </div>
-        </Tooltip>
+        <div style={{ marginTop: '10px' }}>
+          <span className={titleStyle} style={{ position: 'relative', bottom: '3px', paddingRight: 0 }}>
+            Filter by percentile
+          </span>
+          <Tooltip
+            key="tooltip_filter_by_percentile"
+            position={TooltipPosition.right}
+            content={
+              <div style={{ textAlign: 'left' }}>
+                <div>
+                  These percentiles are computed from metrics. To refresh them, reload the page. The filter applies on
+                  span durations. Thus, the filtered traces are the ones where at least one span for the service
+                  satisfies the duration criteria.
+                </div>
+              </div>
+            }
+          >
+            <KialiIcon.Info className={infoStyle} />
+          </Tooltip>
+        </div>
         {percentilesOptions.map(item => {
           let label = item.labelText;
           if (this.computedPercentiles) {
@@ -150,7 +158,27 @@ export class TracesDisplayOptions extends React.Component<Props, State> {
             />
           </label>
         </div>
-        <div className={titleStyle}>Limit per query</div>
+        <div style={{ marginTop: '10px' }}>
+          <span className={titleStyle} style={{ position: 'relative', bottom: '3px', paddingRight: 0 }}>
+            Limit per query
+          </span>
+          <Tooltip
+            key="tooltip_limit_per_query"
+            position={TooltipPosition.right}
+            content={
+              <div style={{ textAlign: 'left' }}>
+                <div>
+                  This limits the number of app-level traces that will be fetched. Because an app may be comprised of
+                  several workload versions not every trace trace may apply to a particular workload. It may be
+                  necessary to increase the limit to get the desired number of workload traces. In some cases the same
+                  can be true of service traces.
+                </div>
+              </div>
+            }
+          >
+            <KialiIcon.Info className={infoStyle} />
+          </Tooltip>
+        </div>
         {[20, 100, 500, 1000].map(limit => (
           <div key={'limit-' + limit}>
             <label key={'limit-' + limit} className={itemStyleWithoutInfo}>
diff --git a/frontend/src/components/Kiosk/KioskActions.ts b/frontend/src/components/Kiosk/KioskActions.ts
index e282b4770..0f377b8fc 100644
--- a/frontend/src/components/Kiosk/KioskActions.ts
+++ b/frontend/src/components/Kiosk/KioskActions.ts
@@ -1,13 +1,19 @@
-import {store} from "../../store/ConfigStore";
-import {Show} from "../../pages/Overview/OverviewPage";
-import {DurationInSeconds, IntervalInMilliseconds, TimeRange} from "../../types/Common";
-import {HEALTHY} from "../../types/Health";
+import { store } from '../../store/ConfigStore';
+import { Show } from '../../pages/Overview/OverviewPage';
+import { DurationInSeconds, IntervalInMilliseconds, TimeRange } from '../../types/Common';
+import { HEALTHY } from '../../types/Health';
 
 // Specific actions that should be communicated to the parent of the Kiosk
 // These actions have Kiali semantic, the parent of the Kiosk should translate them to their specific domain
 // No parent kiosk domain logic should be added here
 
-export const kioskGraphAction = (namespace: string, healthStatus: string, duration: DurationInSeconds, refreshInterval: IntervalInMilliseconds, targetPage: string) => {
+export const kioskGraphAction = (
+  namespace: string,
+  healthStatus: string,
+  duration: DurationInSeconds,
+  refreshInterval: IntervalInMilliseconds,
+  targetPage: string
+) => {
   let showInParent = '/graph/namespaces?namespaces=' + namespace;
   if (healthStatus === HEALTHY.name) {
     showInParent += '&graphFind=healthy';
@@ -27,7 +33,7 @@ export const kioskGraphAction = (namespace: string, healthStatus: string, durati
       break;
   }
   sendParentMessage(showInParent);
-}
+};
 
 export const kioskContextMenuAction = (href: string) => {
   const showInParent = href;
@@ -37,9 +43,14 @@ export const kioskContextMenuAction = (href: string) => {
 export const kioskIstioConfigAction = (namespace: string) => {
   const showInParent = '/istio?namespaces=' + namespace;
   sendParentMessage(showInParent);
-}
+};
 
-export const kioskOverviewAction = (showType: Show, namespace: string, duration: DurationInSeconds, refreshInterval: IntervalInMilliseconds) => {
+export const kioskOverviewAction = (
+  showType: Show,
+  namespace: string,
+  duration: DurationInSeconds,
+  refreshInterval: IntervalInMilliseconds
+) => {
   let showInParent = 'overview';
   switch (showType) {
     case Show.GRAPH:
@@ -56,25 +67,25 @@ export const kioskOverviewAction = (showType: Show, namespace: string, duration:
 export const kioskDurationAction = (duration: DurationInSeconds) => {
   const showInParent = 'duration=' + duration;
   sendParentMessage(showInParent);
-}
+};
 
 export const kioskTimeRangeAction = (timeRange: TimeRange) => {
   const showInParent = 'timeRange=' + JSON.stringify(timeRange);
   sendParentMessage(showInParent);
-}
+};
 
 export const kioskRefreshAction = (refreshInterval: IntervalInMilliseconds) => {
   const showInParent = 'refresh=' + refreshInterval;
   sendParentMessage(showInParent);
-}
+};
 
 export const isKiosk = (kiosk: string): boolean => {
   return kiosk.length > 0;
-}
+};
 
 export const isParentKiosk = (kiosk: string): boolean => {
   return kiosk.length > 0 && kiosk !== 'true';
-}
+};
 
 // Message has no format, parent should parse it for its needs
 const sendParentMessage = (msg: string): void => {
@@ -82,4 +93,4 @@ const sendParentMessage = (msg: string): void => {
   // this will enable iframe -> parent communication
   const targetOrigin = store.getState().globalState.kiosk;
   window.top.postMessage(msg, targetOrigin);
-}
+};
diff --git a/frontend/src/components/Kiosk/KioskElement.tsx b/frontend/src/components/Kiosk/KioskElement.tsx
index 9dd137915..9ccfddc6a 100644
--- a/frontend/src/components/Kiosk/KioskElement.tsx
+++ b/frontend/src/components/Kiosk/KioskElement.tsx
@@ -1,13 +1,13 @@
-import * as React from "react";
-import { useKialiSelector } from "../../hooks/redux";
-import { isKiosk } from "./KioskActions";
+import * as React from 'react';
+import { useKialiSelector } from '../../hooks/redux';
+import { isKiosk } from './KioskActions';
 
 export function KioskElement(props: React.PropsWithChildren<{}>) {
-  const kiosk = useKialiSelector((state) => state.globalState.kiosk);
+  const kiosk = useKialiSelector(state => state.globalState.kiosk);
 
   if (!isKiosk(kiosk)) {
     return null;
   }
 
-  return (<>{props.children}</>);
+  return <>{props.children}</>;
 }
diff --git a/frontend/src/components/Label/_Label.scss b/frontend/src/components/Label/_Label.scss
index 5f9054223..7332d88f0 100644
--- a/frontend/src/components/Label/_Label.scss
+++ b/frontend/src/components/Label/_Label.scss
@@ -4,5 +4,5 @@ And the label color is override by .pf-c-label__content
 And there are not styles with the color white
 */
 .whiteColorLabelContent > .pf-c-label__content {
-  color: #FFF;
+  color: #fff;
 }
diff --git a/frontend/src/components/Link/IstioObjectLink.tsx b/frontend/src/components/Link/IstioObjectLink.tsx
index 984719b81..fb8664692 100644
--- a/frontend/src/components/Link/IstioObjectLink.tsx
+++ b/frontend/src/components/Link/IstioObjectLink.tsx
@@ -6,9 +6,9 @@ import { PFBadge } from 'components/Pf/PfBadges';
 import { Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { KialiIcon } from 'config/KialiIcon';
 import { style } from 'typestyle';
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
-import {isParentKiosk, kioskContextMenuAction} from "../Kiosk/KioskActions";
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
+import { isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions';
 
 export const infoStyle = style({
   margin: '0px 0px -2px 3px'
@@ -16,7 +16,7 @@ export const infoStyle = style({
 
 type ReduxProps = {
   kiosk: string;
-}
+};
 
 type ReferenceIstioObjectProps = {
   name: string;
@@ -24,12 +24,13 @@ type ReferenceIstioObjectProps = {
   type: string;
   subType?: string;
   query?: string;
-}
-
-type IstioObjectProps = ReduxProps & ReferenceIstioObjectProps & {
-  children: React.ReactNode;
 };
 
+type IstioObjectProps = ReduxProps &
+  ReferenceIstioObjectProps & {
+    children: React.ReactNode;
+  };
+
 export const GetIstioObjectUrl = (name: string, namespace: string, type: string, query?: string): string => {
   const istioType = IstioTypes[type];
   let to = '/namespaces/' + namespace + '/' + Paths.ISTIO;
@@ -88,15 +89,19 @@ class IstioObjectLink extends React.Component<IstioObjectProps> {
         onClick={() => {
           kioskContextMenuAction(href);
         }}
-      >{this.props.children}</Link>
+      >
+        {this.props.children}
+      </Link>
     ) : (
-      <Link to={href} data-test={type + '-' + namespace + '-' + name}>{this.props.children}</Link>
+      <Link to={href} data-test={type + '-' + namespace + '-' + name}>
+        {this.props.children}
+      </Link>
     );
   }
 }
 
 const mapStateToProps = (state: KialiAppState): ReduxProps => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
 const IstioObjectLinkContainer = connect(mapStateToProps)(IstioObjectLink);
diff --git a/frontend/src/components/Link/KialiPageLink.tsx b/frontend/src/components/Link/KialiPageLink.tsx
index ea3856c11..58e85626b 100644
--- a/frontend/src/components/Link/KialiPageLink.tsx
+++ b/frontend/src/components/Link/KialiPageLink.tsx
@@ -2,34 +2,39 @@ import * as React from 'react';
 import { Link } from 'react-router-dom';
 import { ExternalLinkAltIcon } from '@patternfly/react-icons';
 import { serverConfig } from '../../config';
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
-import {isParentKiosk, kioskContextMenuAction} from "../Kiosk/KioskActions";
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
+import { isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions';
 
 type ReduxProps = {
-    kiosk: string;
-}
+  kiosk: string;
+};
 
 type KialiPageLinkProps = ReduxProps & {
   children: React.ReactNode;
   cluster?: string;
   href: string;
-}
+};
 
 class KialiPageLink extends React.Component<KialiPageLinkProps> {
-
   render() {
     // Without a cluster, simply render a local link
     // If cluster is specified, and it's the home cluster, render a local link.
-    if (!this.props.cluster || !serverConfig.clusterInfo?.name || this.props.cluster === serverConfig.clusterInfo.name) {
+    if (
+      !this.props.cluster ||
+      !serverConfig.clusterInfo?.name ||
+      this.props.cluster === serverConfig.clusterInfo.name
+    ) {
       if (isParentKiosk(this.props.kiosk)) {
-        return <Link
+        return (
+          <Link
             to={''}
-            onClick={()=> {
+            onClick={() => {
               kioskContextMenuAction(this.props.href);
             }}
             children={this.props.children}
-          />;
+          />
+        );
       } else {
         return <Link to={this.props.href}>{this.props.children}</Link>;
       }
@@ -46,16 +51,15 @@ class KialiPageLink extends React.Component<KialiPageLinkProps> {
       const href = kialiInstance.url.replace(/\/$/g, '') + '/console' + this.props.href;
       return (
         <a href={href} rel="noreferrer noopener" target="_blank">
-          {this.props.children} <ExternalLinkAltIcon/>
+          {this.props.children} <ExternalLinkAltIcon />
         </a>
       );
     }
   }
-
 }
 
 const mapStateToProps = (state: KialiAppState): ReduxProps => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
 const KialiPageLinkContainer = connect(mapStateToProps)(KialiPageLink);
diff --git a/frontend/src/components/Link/KioskLink.tsx b/frontend/src/components/Link/KioskLink.tsx
index d6eace2a1..e974f6dad 100644
--- a/frontend/src/components/Link/KioskLink.tsx
+++ b/frontend/src/components/Link/KioskLink.tsx
@@ -1,18 +1,18 @@
 import * as React from 'react';
 import { Link } from 'react-router-dom';
-import { isParentKiosk, kioskContextMenuAction } from "../Kiosk/KioskActions";
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
+import { isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions';
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
 
 type ReduxProps = {
   kiosk: string;
-}
+};
 
 type Props = {
   linkName: string;
   href: string;
   dataTest: string;
-}
+};
 
 type KioskProps = ReduxProps & Props;
 
@@ -36,7 +36,9 @@ class KioskLinkItem extends React.Component<KioskProps> {
         onClick={() => {
           kioskContextMenuAction(this.props.href);
         }}
-      >{this.props.linkName}</Link>
+      >
+        {this.props.linkName}
+      </Link>
     ) : (
       <Link to={this.props.href} data-test={this.props.dataTest}>
         {this.props.linkName}
@@ -46,7 +48,7 @@ class KioskLinkItem extends React.Component<KioskProps> {
 }
 
 const mapStateToProps = (state: KialiAppState): ReduxProps => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
 const KioskLinkContainer = connect(mapStateToProps)(KioskLinkItem);
diff --git a/frontend/src/components/Link/ServiceLink.tsx b/frontend/src/components/Link/ServiceLink.tsx
index 2e0548559..178fbd01b 100644
--- a/frontend/src/components/Link/ServiceLink.tsx
+++ b/frontend/src/components/Link/ServiceLink.tsx
@@ -2,13 +2,13 @@ import * as React from 'react';
 import { Paths } from '../../config';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { TooltipPosition } from '@patternfly/react-core';
-import {KioskLink} from "./KioskLink";
+import { KioskLink } from './KioskLink';
 
 type Props = {
   name: string;
   namespace: string;
   query?: string;
-}
+};
 
 export const getServiceURL = (name: string, namespace: string, query?: string): string => {
   let to = '/namespaces/' + namespace + '/' + Paths.SERVICES;
@@ -39,6 +39,12 @@ class ServiceLinkItem extends React.Component<Props> {
   render() {
     const { name, namespace, query } = this.props;
     const href = getServiceURL(name, namespace, query);
-    return (<KioskLink linkName={namespace + '/' + name} dataTest={'service-' + namespace + '-' + name} href={href}></KioskLink>)
+    return (
+      <KioskLink
+        linkName={namespace + '/' + name}
+        dataTest={'service-' + namespace + '-' + name}
+        href={href}
+      ></KioskLink>
+    );
   }
 }
diff --git a/frontend/src/components/Link/ValidationSummaryLink.tsx b/frontend/src/components/Link/ValidationSummaryLink.tsx
index ccc223ce4..43718e0b8 100644
--- a/frontend/src/components/Link/ValidationSummaryLink.tsx
+++ b/frontend/src/components/Link/ValidationSummaryLink.tsx
@@ -1,13 +1,13 @@
 import * as React from 'react';
 import { Link } from 'react-router-dom';
 import IstioConfigListLink from './IstioConfigListLink';
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
-import {isParentKiosk, kioskIstioConfigAction} from "../Kiosk/KioskActions";
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
+import { isParentKiosk, kioskIstioConfigAction } from '../Kiosk/KioskActions';
 
 type ReduxProps = {
   kiosk: string;
-}
+};
 
 type Props = ReduxProps & {
   namespace: string;
@@ -15,7 +15,7 @@ type Props = ReduxProps & {
   warnings: number;
   objectCount?: number;
   children: React.ReactNode;
-}
+};
 
 class ValidationSummaryLink extends React.Component<Props> {
   hasIstioObjects = () => {
@@ -29,10 +29,10 @@ class ValidationSummaryLink extends React.Component<Props> {
       // Kiosk actions are used when the kiosk specifies a parent,
       // otherwise the kiosk=true will keep the links inside Kiali
       link = isParentKiosk(this.props.kiosk) ? (
-          <Link to={''} onClick={() => kioskIstioConfigAction(this.props.namespace)}>
-            {this.props.children}
-          </Link>
-        ) : (
+        <Link to={''} onClick={() => kioskIstioConfigAction(this.props.namespace)}>
+          {this.props.children}
+        </Link>
+      ) : (
         <IstioConfigListLink
           namespaces={[this.props.namespace]}
           warnings={this.props.warnings > 0}
@@ -48,8 +48,8 @@ class ValidationSummaryLink extends React.Component<Props> {
 }
 
 const mapStateToProps = (state: KialiAppState) => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
-const ValidationSummaryLinkContainer = connect(mapStateToProps)(ValidationSummaryLink)
+const ValidationSummaryLinkContainer = connect(mapStateToProps)(ValidationSummaryLink);
 export default ValidationSummaryLinkContainer;
diff --git a/frontend/src/components/Link/WorkloadLink.tsx b/frontend/src/components/Link/WorkloadLink.tsx
index 1badbb1aa..77cd7b13d 100644
--- a/frontend/src/components/Link/WorkloadLink.tsx
+++ b/frontend/src/components/Link/WorkloadLink.tsx
@@ -2,13 +2,13 @@ import * as React from 'react';
 import { Paths } from '../../config';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { TooltipPosition } from '@patternfly/react-core';
-import {KioskLink} from "./KioskLink";
+import { KioskLink } from './KioskLink';
 
 type Props = {
   name: string;
   namespace: string;
   query?: string;
-}
+};
 
 export const getWorkloadLink = (name: string, namespace: string, query?: string): string => {
   let to = '/namespaces/' + namespace + '/' + Paths.WORKLOADS;
@@ -39,6 +39,12 @@ class WorkloadLinkItem extends React.Component<Props> {
   render() {
     const { name, namespace, query } = this.props;
     const href = getWorkloadLink(name, namespace, query);
-    return (<KioskLink linkName={namespace + '/' + name} dataTest={'workload-' + namespace + '-' + name} href={href}></KioskLink>)
+    return (
+      <KioskLink
+        linkName={namespace + '/' + name}
+        dataTest={'workload-' + namespace + '-' + name}
+        href={href}
+      ></KioskLink>
+    );
   }
 }
diff --git a/frontend/src/components/MTls/MeshMTLSStatus.tsx b/frontend/src/components/MTls/MeshMTLSStatus.tsx
index 1b81a3f62..d1b3868c6 100644
--- a/frontend/src/components/MTls/MeshMTLSStatus.tsx
+++ b/frontend/src/components/MTls/MeshMTLSStatus.tsx
@@ -4,11 +4,7 @@ import { KialiAppState } from '../../store/Store';
 import { MTLSIconTypes } from './MTLSIcon';
 import { default as MTLSStatus, emptyDescriptor, StatusDescriptor } from './MTLSStatus';
 import { style } from 'typestyle';
-import {
-  meshWideMTLSEnabledSelector,
-  meshWideMTLSStatusSelector,
-  namespaceItemsSelector
-} from '../../store/Selectors';
+import { meshWideMTLSEnabledSelector, meshWideMTLSStatusSelector, namespaceItemsSelector } from '../../store/Selectors';
 import { connect } from 'react-redux';
 import { MTLSStatuses, TLSStatus } from '../../types/TLSStatus';
 import * as AlertUtils from '../../utils/AlertUtils';
@@ -19,7 +15,7 @@ import { bindActionCreators } from 'redux';
 import { MeshTlsActions } from '../../actions/MeshTlsActions';
 import { TimeInMilliseconds } from '../../types/Common';
 import Namespace from '../../types/Namespace';
-import connectRefresh from "../Refresh/connectRefresh";
+import connectRefresh from '../Refresh/connectRefresh';
 
 type ReduxProps = {
   setMeshTlsStatus: (meshStatus: TLSStatus) => void;
@@ -65,6 +61,14 @@ const statusDescriptors = new Map<string, StatusDescriptor>([
       showStatus: true
     }
   ],
+  [
+    MTLSStatuses.AUTO_DEFAULT,
+    {
+      message: 'Automatic Mesh-wide mTLS is enabled',
+      icon: MTLSIconTypes.LOCK_FULL,
+      showStatus: true
+    }
+  ],
   [MTLSStatuses.NOT_ENABLED, emptyDescriptor]
 ]);
 
@@ -106,17 +110,20 @@ class MeshMTLSStatus extends React.Component<Props> {
   finalStatus() {
     if (this.props.autoMTLSEnabled) {
       if (this.props.status === MTLSStatuses.ENABLED) {
-        return MTLSStatuses.ENABLED_DEFAULT
+        return MTLSStatuses.ENABLED_DEFAULT;
       }
       if (this.props.status === MTLSStatuses.PARTIALLY) {
-        return MTLSStatuses.PARTIALLY_DEFAULT
+        return MTLSStatuses.PARTIALLY_DEFAULT;
       }
+      return MTLSStatuses.AUTO_DEFAULT;
     }
-    return this.props.status
+    return this.props.status;
   }
 
   render() {
-    return <MTLSStatus className={this.iconStyle()} status={this.finalStatus()} statusDescriptors={statusDescriptors} />;
+    return (
+      <MTLSStatus className={this.iconStyle()} status={this.finalStatus()} statusDescriptors={statusDescriptors} />
+    );
   }
 }
 
diff --git a/frontend/src/components/MessageCenter/MessageCenterTrigger.tsx b/frontend/src/components/MessageCenter/MessageCenterTrigger.tsx
index 76aa48b08..d817f1eba 100644
--- a/frontend/src/components/MessageCenter/MessageCenterTrigger.tsx
+++ b/frontend/src/components/MessageCenter/MessageCenterTrigger.tsx
@@ -16,9 +16,9 @@ type PropsType = {
   toggleSystemErrorsCenter: () => void;
 };
 
-const systemErrorCountStyle = style ({
-  marginRight: "0.3em",
-  paddingTop: "0.1em"
+const systemErrorCountStyle = style({
+  marginRight: '0.3em',
+  paddingTop: '0.1em'
 });
 
 export class MessageCenterTrigger extends React.PureComponent<PropsType, {}> {
@@ -59,7 +59,7 @@ export class MessageCenterTrigger extends React.PureComponent<PropsType, {}> {
     const count = style({
       position: 'relative',
       top: '2px',
-      verticalAlign: "0.125em"
+      verticalAlign: '0.125em'
     });
 
     return (
diff --git a/frontend/src/components/Metrics/CustomMetrics.tsx b/frontend/src/components/Metrics/CustomMetrics.tsx
index 7826c44f2..9478d6c29 100644
--- a/frontend/src/components/Metrics/CustomMetrics.tsx
+++ b/frontend/src/components/Metrics/CustomMetrics.tsx
@@ -9,7 +9,9 @@ import {
   CardBody,
   Checkbox,
   EmptyState,
-  EmptyStateVariant, Title, TitleSizes
+  EmptyStateVariant,
+  Title,
+  TitleSizes
 } from '@patternfly/react-core';
 import { style } from 'typestyle';
 import { serverConfig } from '../../config/ServerConfig';
@@ -20,11 +22,11 @@ import { TimeRange, evalTimeRange, TimeInMilliseconds, isEqualTimeRange } from '
 import * as AlertUtils from '../../utils/AlertUtils';
 import { RenderComponentScroll } from '../../components/Nav/Page';
 import * as MetricsHelper from './Helper';
-import { KioskElement } from "../Kiosk/KioskElement";
+import { KioskElement } from '../Kiosk/KioskElement';
 import { MetricsSettings, LabelsSettings } from '../MetricsOptions/MetricsSettings';
 import { MetricsSettingsDropdown } from '../MetricsOptions/MetricsSettingsDropdown';
 import MetricsRawAggregation from '../MetricsOptions/MetricsRawAggregation';
-import { TimeDurationModal } from "../Time/TimeDurationModal";
+import { TimeDurationModal } from '../Time/TimeDurationModal';
 import { GrafanaLinks } from './GrafanaLinks';
 import { MetricsObjectTypes } from 'types/Metrics';
 import { SpanOverlay, JaegerLineInfo } from './SpanOverlay';
@@ -37,7 +39,7 @@ import { KialiDispatch } from 'types/Redux';
 import { bindActionCreators } from 'redux';
 import { UserSettingsActions } from '../../actions/UserSettingsActions';
 import { timeRangeSelector } from '../../store/Selectors';
-import TimeDurationIndicatorContainer from "../Time/TimeDurationIndicatorComponent";
+import TimeDurationIndicatorContainer from '../Time/TimeDurationIndicatorComponent';
 
 type MetricsState = {
   dashboard?: DashboardModel;
@@ -217,7 +219,7 @@ class CustomMetrics extends React.Component<Props, MetricsState> {
     }
   }
 
-  renderFetchMetrics = (title) => {
+  renderFetchMetrics = title => {
     return (
       <div className={emptyStyle}>
         <EmptyState variant={EmptyStateVariant.small}>
@@ -276,7 +278,8 @@ class CustomMetrics extends React.Component<Props, MetricsState> {
           customDuration={true}
           isOpen={this.state.isTimeOptionsOpen}
           onConfirm={this.toggleTimeOptionsVisibility}
-          onCancel={this.toggleTimeOptionsVisibility} />
+          onCancel={this.toggleTimeOptionsVisibility}
+        />
       </>
     );
   }
@@ -352,7 +355,7 @@ class CustomMetrics extends React.Component<Props, MetricsState> {
   };
 
   private toggleTimeOptionsVisibility = () => {
-    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }) );
+    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }));
   };
 }
 
diff --git a/frontend/src/components/Metrics/IstioMetrics.tsx b/frontend/src/components/Metrics/IstioMetrics.tsx
index 597003898..6b8887b3d 100644
--- a/frontend/src/components/Metrics/IstioMetrics.tsx
+++ b/frontend/src/components/Metrics/IstioMetrics.tsx
@@ -7,22 +7,16 @@ import { Card, CardBody, Checkbox, Toolbar, ToolbarGroup, ToolbarItem } from '@p
 import { style } from 'typestyle';
 import * as API from 'services/Api';
 import { KialiAppState } from 'store/Store';
-import {
-  TimeRange,
-  evalTimeRange,
-  TimeInMilliseconds,
-  isEqualTimeRange,
-  IntervalInMilliseconds
-} from 'types/Common';
+import { TimeRange, evalTimeRange, TimeInMilliseconds, isEqualTimeRange, IntervalInMilliseconds } from 'types/Common';
 import { Direction, IstioMetricsOptions, Reporter } from 'types/MetricsOptions';
 import * as AlertUtils from 'utils/AlertUtils';
 import { RenderComponentScroll } from 'components/Nav/Page';
 import * as MetricsHelper from './Helper';
-import { KioskElement } from "../Kiosk/KioskElement";
+import { KioskElement } from '../Kiosk/KioskElement';
 import { MetricsSettings, LabelsSettings } from '../MetricsOptions/MetricsSettings';
 import { MetricsSettingsDropdown } from '../MetricsOptions/MetricsSettingsDropdown';
 import MetricsReporter from '../MetricsOptions/MetricsReporter';
-import { TimeDurationModal } from "../Time/TimeDurationModal";
+import { TimeDurationModal } from '../Time/TimeDurationModal';
 import history, { URLParam } from 'app/History';
 import { MetricsObjectTypes } from 'types/Metrics';
 import { GrafanaInfo } from 'types/GrafanaInfo';
@@ -36,7 +30,7 @@ import { Dashboard } from 'components/Charts/Dashboard';
 import { refreshIntervalSelector, timeRangeSelector } from 'store/Selectors';
 import { UserSettingsActions } from 'actions/UserSettingsActions';
 import { KialiCrippledFeatures } from 'types/ServerConfig';
-import TimeDurationIndicatorContainer from "../Time/TimeDurationIndicatorComponent";
+import TimeDurationIndicatorContainer from '../Time/TimeDurationIndicatorComponent';
 
 type MetricsState = {
   crippledFeatures?: KialiCrippledFeatures;
@@ -60,7 +54,7 @@ type IstioMetricsProps = ObjectId &
     objectType: MetricsObjectTypes;
     direction: Direction;
   } & {
-  lastRefreshAt: TimeInMilliseconds;
+    lastRefreshAt: TimeInMilliseconds;
   };
 
 type ReduxProps = {
@@ -309,7 +303,8 @@ class IstioMetrics extends React.Component<Props, MetricsState> {
           customDuration={true}
           isOpen={this.state.isTimeOptionsOpen}
           onConfirm={this.toggleTimeOptionsVisibility}
-          onCancel={this.toggleTimeOptionsVisibility} />
+          onCancel={this.toggleTimeOptionsVisibility}
+        />
       </>
     );
   }
@@ -329,8 +324,8 @@ class IstioMetrics extends React.Component<Props, MetricsState> {
   };
 
   private toggleTimeOptionsVisibility = () => {
-    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }) );
-  }
+    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }));
+  };
 
   private renderOptionsBar() {
     const hasHistogramsAverage =
@@ -342,7 +337,7 @@ class IstioMetrics extends React.Component<Props, MetricsState> {
       !this.state.crippledFeatures?.responseSizePercentiles ||
       !this.state.crippledFeatures?.responseTimePercentiles;
 
-      return (
+    return (
       <div ref={this.toolbarRef}>
         <Toolbar style={{ padding: 0, marginBottom: '20px' }}>
           <ToolbarGroup>
diff --git a/frontend/src/components/Metrics/SpanOverlay.ts b/frontend/src/components/Metrics/SpanOverlay.ts
index 32862d9fa..bedc95256 100644
--- a/frontend/src/components/Metrics/SpanOverlay.ts
+++ b/frontend/src/components/Metrics/SpanOverlay.ts
@@ -78,7 +78,11 @@ export class SpanOverlay {
           symbol: 'circle',
           size: 10
         },
-        dataStyle: { fill: ({ datum }) => (datum.error ? PFColors.Danger : PFColors.Cyan300), fillOpacity: 0.6, cursor: 'pointer' },
+        dataStyle: {
+          fill: ({ datum }) => (datum.error ? PFColors.Danger : PFColors.Cyan300),
+          fillOpacity: 0.6,
+          cursor: 'pointer'
+        },
         buckets: this.spans.length > 1000 ? 15 : 0
       };
       const dps = this.spans.map(span => {
diff --git a/frontend/src/components/Metrics/__tests__/IstioMetrics.test.tsx b/frontend/src/components/Metrics/__tests__/IstioMetrics.test.tsx
index bc46ceac0..d65e88554 100644
--- a/frontend/src/components/Metrics/__tests__/IstioMetrics.test.tsx
+++ b/frontend/src/components/Metrics/__tests__/IstioMetrics.test.tsx
@@ -124,7 +124,13 @@ describe('Metrics for a service', () => {
     new MounterMocker()
       .addMock('getServiceDashboard', dashboard)
       .mountWithStore(
-        <IstioMetrics namespace="ns" object="svc" objectType={MetricsObjectTypes.SERVICE} direction={'inbound'} lastRefreshAt={Date.now()} />
+        <IstioMetrics
+          namespace="ns"
+          object="svc"
+          objectType={MetricsObjectTypes.SERVICE}
+          direction={'inbound'}
+          lastRefreshAt={Date.now()}
+        />
       )
       .run(done, wrapper => {
         expect(wrapper.find('Chart')).toHaveLength(0);
@@ -147,7 +153,13 @@ describe('Metrics for a service', () => {
     new MounterMocker()
       .addMock('getServiceDashboard', dashboard)
       .mountWithStore(
-        <IstioMetrics namespace="ns" object="svc" objectType={MetricsObjectTypes.SERVICE} direction={'inbound'} lastRefreshAt={Date.now()} />
+        <IstioMetrics
+          namespace="ns"
+          object="svc"
+          objectType={MetricsObjectTypes.SERVICE}
+          direction={'inbound'}
+          lastRefreshAt={Date.now()}
+        />
       )
       .run(done, wrapper => {
         expect(wrapper.find('Chart')).toHaveLength(4);
@@ -199,7 +211,13 @@ describe('Inbound Metrics for a workload', () => {
     new MounterMocker()
       .addMock('getWorkloadDashboard', dashboard)
       .mountWithStore(
-        <IstioMetrics namespace="ns" object="wkd" objectType={MetricsObjectTypes.WORKLOAD} direction={'inbound'} lastRefreshAt={Date.now()} />
+        <IstioMetrics
+          namespace="ns"
+          object="wkd"
+          objectType={MetricsObjectTypes.WORKLOAD}
+          direction={'inbound'}
+          lastRefreshAt={Date.now()}
+        />
       )
       .run(done, wrapper => {
         expect(wrapper.find('Chart')).toHaveLength(0);
@@ -222,7 +240,13 @@ describe('Inbound Metrics for a workload', () => {
     new MounterMocker()
       .addMock('getWorkloadDashboard', dashboard)
       .mountWithStore(
-        <IstioMetrics namespace="ns" object="wkd" objectType={MetricsObjectTypes.WORKLOAD} direction={'inbound'} lastRefreshAt={Date.now()} />
+        <IstioMetrics
+          namespace="ns"
+          object="wkd"
+          objectType={MetricsObjectTypes.WORKLOAD}
+          direction={'inbound'}
+          lastRefreshAt={Date.now()}
+        />
       )
       .run(done, wrapper => {
         expect(wrapper.find('Chart')).toHaveLength(4);
diff --git a/frontend/src/components/MissingSidecar/MissingSidecar.tsx b/frontend/src/components/MissingSidecar/MissingSidecar.tsx
index 7aaf89994..ed4d2d03b 100644
--- a/frontend/src/components/MissingSidecar/MissingSidecar.tsx
+++ b/frontend/src/components/MissingSidecar/MissingSidecar.tsx
@@ -7,7 +7,7 @@ import { KialiIcon } from '../../config/KialiIcon';
 import { style } from 'typestyle';
 
 type MissingSidecarProps = {
-  "data-test"?: string;
+  'data-test'?: string;
   text: string;
   textTooltip: string;
   tooltip: boolean;
@@ -37,7 +37,7 @@ class MissingSidecar extends React.Component<MissingSidecarProps, {}> {
     const { text, textTooltip, icon, namespace, color, tooltip, style, ...otherProps } = this.props;
 
     const iconComponent = (
-      <span style={style} {...otherProps} data-test={this.props["data-test"]}>
+      <span style={style} {...otherProps} data-test={this.props['data-test']}>
         {React.createElement(icon, { style: { color: color, verticalAlign: '-2px' } })}
         {!tooltip && (
           <span style={{ marginLeft: '8px' }}>
diff --git a/frontend/src/components/Nav/Masthead/HelpDropdown.tsx b/frontend/src/components/Nav/Masthead/HelpDropdown.tsx
index aa4959367..7bd6eb657 100644
--- a/frontend/src/components/Nav/Masthead/HelpDropdown.tsx
+++ b/frontend/src/components/Nav/Masthead/HelpDropdown.tsx
@@ -79,7 +79,7 @@ class HelpDropdownContainer extends React.Component<HelpDropdownProps, HelpDropd
         toggleIndicator={null}
         onToggle={this.onDropdownToggle}
         aria-label="Help"
-        style={{ marginTop: 3, verticalAlign: "-0.1em" }}
+        style={{ marginTop: 3, verticalAlign: '-0.1em' }}
       >
         <QuestionCircleIcon />
       </DropdownToggle>
@@ -94,7 +94,12 @@ class HelpDropdownContainer extends React.Component<HelpDropdownProps, HelpDropd
     );
 
     items.push(
-      <DropdownItem component={'span'} key={'view_debug_info'} onClick={this.openDebugInformation}>
+      <DropdownItem
+        component={'span'}
+        key={'view_debug_info'}
+        onClick={this.openDebugInformation}
+        style={{ cursor: 'pointer' }}
+      >
         View Debug Info
       </DropdownItem>
     );
@@ -119,7 +124,8 @@ class HelpDropdownContainer extends React.Component<HelpDropdownProps, HelpDropd
           ref={this.about}
           status={this.props.status}
           externalServices={this.props.externalServices}
-          warningMessages={this.props.warningMessages} />
+          warningMessages={this.props.warningMessages}
+        />
         <DebugInformationContainer ref={this.debugInformation} />
         {serverConfig.kialiFeatureFlags.certificatesInformationIndicators.enabled && (
           <IstioCertsInfoConnected ref={this.certsInformation} />
diff --git a/frontend/src/components/Nav/Masthead/UserDropdown.tsx b/frontend/src/components/Nav/Masthead/UserDropdown.tsx
index 3b53df1b2..fa246bbfc 100644
--- a/frontend/src/components/Nav/Masthead/UserDropdown.tsx
+++ b/frontend/src/components/Nav/Masthead/UserDropdown.tsx
@@ -112,8 +112,8 @@ class UserDropdownConnected extends React.Component<UserProps, UserState> {
 
   render() {
     const { isDropdownOpen } = this.state;
-    const canLogout = authenticationConfig.strategy !== AuthStrategy.anonymous &&
-      authenticationConfig.strategy !== AuthStrategy.header;
+    const canLogout =
+      authenticationConfig.strategy !== AuthStrategy.anonymous && authenticationConfig.strategy !== AuthStrategy.header;
 
     const userDropdownItems = (
       <DropdownItem key={'user_logout_option'} onClick={this.handleLogout} isDisabled={!canLogout}>
@@ -129,9 +129,7 @@ class UserDropdownConnected extends React.Component<UserProps, UserState> {
           show={this.state.showSessionTimeOut && !this.state.isSessionTimeoutDismissed}
           timeOutCountDown={this.state.timeCountDownSeconds}
         />
-        {this.props.session && !canLogout && (
-          <>{this.props.session.username}</>
-        )}
+        {this.props.session && !canLogout && <>{this.props.session.username}</>}
         {this.props.session && canLogout && (
           <Dropdown
             isPlain={true}
diff --git a/frontend/src/components/Nav/NavUtils.tsx b/frontend/src/components/Nav/NavUtils.tsx
index 7da1e9c21..4b8ce1968 100644
--- a/frontend/src/components/Nav/NavUtils.tsx
+++ b/frontend/src/components/Nav/NavUtils.tsx
@@ -2,7 +2,7 @@ import { Layout, EdgeLabelMode, NodeType, NodeParamsType, GraphType, TrafficRate
 import { DurationInSeconds, IntervalInMilliseconds } from '../../types/Common';
 import Namespace from '../../types/Namespace';
 import { URLParam } from '../../app/History';
-import {getKioskMode, isKioskMode} from '../../utils/SearchParamUtils';
+import { getKioskMode, isKioskMode } from '../../utils/SearchParamUtils';
 
 export type GraphUrlParams = {
   activeNamespaces: Namespace[];
diff --git a/frontend/src/components/Nav/Page/RenderComponentScroll.tsx b/frontend/src/components/Nav/Page/RenderComponentScroll.tsx
index c5caf0798..3df04adc4 100644
--- a/frontend/src/components/Nav/Page/RenderComponentScroll.tsx
+++ b/frontend/src/components/Nav/Page/RenderComponentScroll.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
-import {store} from "../../../store/ConfigStore";
-import {isKiosk} from "../../Kiosk/KioskActions";
+import { store } from '../../../store/ConfigStore';
+import { isKiosk } from '../../Kiosk/KioskActions';
 
 // TOP_PADDING constant is used to adjust the height of the main div to allow scrolling in the inner container layer.
 export const TOP_PADDING = 76 + 118;
diff --git a/frontend/src/components/Nav/Page/RenderHeader.tsx b/frontend/src/components/Nav/Page/RenderHeader.tsx
index e9189f10b..e2edbd96c 100644
--- a/frontend/src/components/Nav/Page/RenderHeader.tsx
+++ b/frontend/src/components/Nav/Page/RenderHeader.tsx
@@ -2,9 +2,9 @@ import React from 'react';
 import { style } from 'typestyle';
 import { PFColors } from '../../Pf/PfColors';
 import BreadcrumbView from '../../BreadcrumbView/BreadcrumbView';
-import {KialiAppState} from "../../../store/Store";
-import {connect} from "react-redux";
-import {isKiosk} from "../../Kiosk/KioskActions";
+import { KialiAppState } from '../../../store/Store';
+import { connect } from 'react-redux';
+import { isKiosk } from '../../Kiosk/KioskActions';
 
 const containerPadding = style({ padding: '0 20px 28px 20px' });
 const containerWhite = style({ backgroundColor: PFColors.White });
@@ -31,7 +31,7 @@ const actionsToolbarStyle = style({
 
 type ReduxProps = {
   kiosk: string;
-}
+};
 
 type RenderHeaderProps = ReduxProps & {
   location?: {
@@ -40,13 +40,13 @@ type RenderHeaderProps = ReduxProps & {
   };
   rightToolbar?: JSX.Element;
   actionsToolbar?: JSX.Element;
-}
+};
 
 export class RenderHeader extends React.Component<RenderHeaderProps> {
   render() {
     // RenderHeader is used only in the detail pages
     // On kiosk mode, it should be hidden
-    return isKiosk(this.props.kiosk) ? (null) : (
+    return isKiosk(this.props.kiosk) ? null : (
       <>
         <div className={`${containerPadding} ${containerWhite}`}>
           {this.props.location && (
@@ -68,7 +68,7 @@ export class RenderHeader extends React.Component<RenderHeaderProps> {
 }
 
 const mapStateToProps = (state: KialiAppState) => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
 const RenderHeaderContainer = connect(mapStateToProps)(RenderHeader);
diff --git a/frontend/src/components/Overview/TLSInfo.tsx b/frontend/src/components/Overview/TLSInfo.tsx
index 91c5bd40e..2d580cf58 100644
--- a/frontend/src/components/Overview/TLSInfo.tsx
+++ b/frontend/src/components/Overview/TLSInfo.tsx
@@ -1,16 +1,16 @@
-import {Label, Tooltip, TooltipPosition} from '@patternfly/react-core';
+import { Label, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { KialiIcon } from 'config/KialiIcon';
 import * as React from 'react';
-import {style} from "typestyle";
-import {KialiAppState} from "../../store/Store";
-import {istioCertsInfoSelector} from "../../store/Selectors";
-import {CertsInfo} from "../../types/CertsInfo";
-import {connect} from "react-redux";
-import {infoStyle} from "../../pages/Overview/OverviewCardControlPlaneNamespace";
+import { style } from 'typestyle';
+import { KialiAppState } from '../../store/Store';
+import { istioCertsInfoSelector } from '../../store/Selectors';
+import { CertsInfo } from '../../types/CertsInfo';
+import { connect } from 'react-redux';
+import { infoStyle } from '../../pages/Overview/OverviewCardControlPlaneNamespace';
 
 type Props = {
-  certificatesInformationIndicators: boolean,
-  version?: string,
+  certificatesInformationIndicators: boolean;
+  version?: string;
   certsInfo: CertsInfo[];
 };
 
@@ -18,76 +18,76 @@ const lockIconStyle = style({ marginLeft: '5px' });
 
 function showCerts(certs) {
   if (certs) {
-    let rows = certs.map(function(item) {
-      return (<div key={"showCerts"}>
-        <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>From {item.issuer}</div>
-        <div>
-          <div>Issuer: </div>
-          <div>{item.secretName}</div>
-        </div>
-        <div>
-          <div>Valid From: </div>
-          <div>{item.notAfter}</div>
-        </div>
-        <div>
-          <div>Valid To: </div>
-          <div>{item.notBefore}</div>
+    let rows = certs.map(function (item) {
+      return (
+        <div key={'showCerts'}>
+          <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>From {item.issuer}</div>
+          <div>
+            <div>Issuer: </div>
+            <div>{item.secretName}</div>
+          </div>
+          <div>
+            <div>Valid From: </div>
+            <div>{item.notAfter}</div>
+          </div>
+          <div>
+            <div>Valid To: </div>
+            <div>{item.notBefore}</div>
+          </div>
         </div>
-      </div>)
+      );
     });
-    return (
-      <div>
-        {rows}
-      </div>
-    );
+    return <div>{rows}</div>;
   } else {
-    return ("No cert info")
+    return 'No cert info';
   }
-
 }
 
 function LockIcon(props) {
-
-  return (
-    props.certificatesInformationIndicators === true ? (
-      <Tooltip
-        position={TooltipPosition.top}
-        content={showCerts(props.certsInfo)}
-      >
-        <div data-test={"lockerCA"}>
-          <KialiIcon.MtlsLock className={lockIconStyle}/>
-        </div>
-      </Tooltip>)
-      : (<KialiIcon.MtlsLock className={lockIconStyle}/>)
+  return props.certificatesInformationIndicators === true ? (
+    <Tooltip position={TooltipPosition.top} content={showCerts(props.certsInfo)}>
+      <div data-test={'lockerCA'}>
+        <KialiIcon.MtlsLock className={lockIconStyle} />
+      </div>
+    </Tooltip>
+  ) : (
+    <KialiIcon.MtlsLock className={lockIconStyle} />
   );
-};
+}
 
 class TLSInfo extends React.Component<Props> {
-
   render() {
     return (
       <div style={{ textAlign: 'left' }}>
-          <div>
-            <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Min TLS version</div>
-              <Label isCompact color="blue" data-test={"label-TLS"}>
-                {this.props.version} <LockIcon certificatesInformationIndicators={this.props.certificatesInformationIndicators} certsInfo={this.props.certsInfo}></LockIcon>
-                <Tooltip
-                  position={TooltipPosition.right}
-                  content={<div style={{ textAlign: 'left' }}>The meshConfig.meshMTLS.minProtocolVersion field specifies the minimum TLS version for the TLS connections among Istio workloads. N/A if it was not set.</div>}
-                >
-                  <KialiIcon.Info className={infoStyle} />
-                </Tooltip>
-              </Label>
-          </div>
+        <div>
+          <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Min TLS version</div>
+          <Label isCompact color="blue" data-test={'label-TLS'}>
+            {this.props.version}{' '}
+            <LockIcon
+              certificatesInformationIndicators={this.props.certificatesInformationIndicators}
+              certsInfo={this.props.certsInfo}
+            ></LockIcon>
+            <Tooltip
+              position={TooltipPosition.right}
+              content={
+                <div style={{ textAlign: 'left' }}>
+                  The meshConfig.meshMTLS.minProtocolVersion field specifies the minimum TLS version for the TLS
+                  connections among Istio workloads. N/A if it was not set.
+                </div>
+              }
+            >
+              <KialiIcon.Info className={infoStyle} />
+            </Tooltip>
+          </Label>
+        </div>
       </div>
     );
-  };
+  }
 }
 
 const mapStateToProps = (state: KialiAppState) => ({
-  certsInfo: istioCertsInfoSelector(state),
+  certsInfo: istioCertsInfoSelector(state)
 });
 
 const TLSInfoConnect = connect(mapStateToProps)(TLSInfo);
 export default TLSInfoConnect;
-
diff --git a/frontend/src/components/Pf/PfBadges.tsx b/frontend/src/components/Pf/PfBadges.tsx
index 11e21d4d1..b25dedc01 100644
--- a/frontend/src/components/Pf/PfBadges.tsx
+++ b/frontend/src/components/Pf/PfBadges.tsx
@@ -121,13 +121,7 @@ export class PFBadge extends React.PureComponent<PFBadgeProps> {
     const className = this.props.size === 'sm' ? kialiBadgeSmall : kialiBadge;
 
     const badge = (
-      <Badge
-        className={className}
-        id={key}
-        isRead={this.props.isRead || false}
-        key={key}
-        style={style}
-      >
+      <Badge className={className} id={key} isRead={this.props.isRead || false} key={key} style={style}>
         {this.props.badge.badge}
       </Badge>
     );
diff --git a/frontend/src/components/Refresh/Refresh.tsx b/frontend/src/components/Refresh/Refresh.tsx
index 4323fc426..d80f35ab0 100644
--- a/frontend/src/components/Refresh/Refresh.tsx
+++ b/frontend/src/components/Refresh/Refresh.tsx
@@ -10,9 +10,9 @@ import { ToolbarDropdown } from '../ToolbarDropdown/ToolbarDropdown';
 import RefreshButtonContainer from './RefreshButton';
 import { HistoryManager, URLParam } from 'app/History';
 import { TooltipPosition } from '@patternfly/react-core';
-import { triggerRefresh } from "../../hooks/refresh";
-import {isKioskMode} from "../../utils/SearchParamUtils";
-import {kioskRefreshAction} from "../Kiosk/KioskActions";
+import { triggerRefresh } from '../../hooks/refresh';
+import { isKioskMode } from '../../utils/SearchParamUtils';
+import { kioskRefreshAction } from '../Kiosk/KioskActions';
 
 type ReduxProps = {
   refreshInterval: IntervalInMilliseconds;
@@ -72,7 +72,9 @@ export class Refresh extends React.PureComponent<Props> {
             tooltip={'Refresh interval'}
             tooltipPosition={TooltipPosition.left}
           />
-          {this.props.hideRefreshButton || <RefreshButtonContainer handleRefresh={triggerRefresh} disabled={this.props.disabled}/>}
+          {this.props.hideRefreshButton || (
+            <RefreshButtonContainer handleRefresh={triggerRefresh} disabled={this.props.disabled} />
+          )}
         </>
       );
     } else {
@@ -82,7 +84,7 @@ export class Refresh extends React.PureComponent<Props> {
 
   private updateRefreshInterval = (refreshInterval: IntervalInMilliseconds) => {
     this.props.setRefreshInterval(refreshInterval); // notify redux of the change
-    if (isKioskMode() ) {
+    if (isKioskMode()) {
       kioskRefreshAction(refreshInterval);
     }
   };
diff --git a/frontend/src/components/Refresh/RefreshNotifier.tsx b/frontend/src/components/Refresh/RefreshNotifier.tsx
index 22412b9ea..da730de89 100644
--- a/frontend/src/components/Refresh/RefreshNotifier.tsx
+++ b/frontend/src/components/Refresh/RefreshNotifier.tsx
@@ -1,6 +1,6 @@
-import { useEffect } from "react";
-import { TimeInMilliseconds } from "../../types/Common";
-import useRefreshInterval from "../../hooks/refresh";
+import { useEffect } from 'react';
+import { TimeInMilliseconds } from '../../types/Common';
+import useRefreshInterval from '../../hooks/refresh';
 
 interface Props {
   onTick: (timestamp: TimeInMilliseconds) => void;
diff --git a/frontend/src/components/Refresh/connectRefresh.tsx b/frontend/src/components/Refresh/connectRefresh.tsx
index b8f8a33f8..a98f29674 100644
--- a/frontend/src/components/Refresh/connectRefresh.tsx
+++ b/frontend/src/components/Refresh/connectRefresh.tsx
@@ -1,5 +1,5 @@
-import * as React from "react";
-import useRefreshInterval from "../../hooks/refresh";
+import * as React from 'react';
+import useRefreshInterval from '../../hooks/refresh';
 
 // TODO: This is a transition component. Please, avoid using this HOC/function.
 //  Prefer usage of the useRefreshInterval hook for function components, or the RefreshNotifier component for class components.
@@ -7,8 +7,6 @@ export default function connectRefresh<P extends { lastRefreshAt: number }>(Comp
   return React.forwardRef((props: Omit<P, 'lastRefreshAt'>, ref) => {
     const refreshing = useRefreshInterval();
 
-    return (
-      <Component {...props as P} ref={ref} lastRefreshAt={refreshing.lastRefreshAt} />
-    );
+    return <Component {...(props as P)} ref={ref} lastRefreshAt={refreshing.lastRefreshAt} />;
   });
 }
diff --git a/frontend/src/components/SummaryPanel/ResponseTimeChart.tsx b/frontend/src/components/SummaryPanel/ResponseTimeChart.tsx
index 5b52355a0..e89eed973 100644
--- a/frontend/src/components/SummaryPanel/ResponseTimeChart.tsx
+++ b/frontend/src/components/SummaryPanel/ResponseTimeChart.tsx
@@ -54,7 +54,7 @@ export class ResponseTimeChart extends React.Component<ResponseTimeChartTypeProp
                 padding={{ top: 5 }}
                 tooltipFormat={dp => {
                   const val = Math.floor(dp.y * 1000) / 1000;
-                  return `${(dp.x as Date).toLocaleTimeString()} - ${dp.name}: ${val.toFixed(2)} ms`;
+                  return `${(dp.x as Date).toLocaleStringWithConditionalDate()} - ${dp.name}: ${val.toFixed(2)} ms`;
                 }}
                 series={series}
                 labelName="ops"
diff --git a/frontend/src/components/SummaryPanel/RpsChart.tsx b/frontend/src/components/SummaryPanel/RpsChart.tsx
index 23ce55ceb..b981b3073 100644
--- a/frontend/src/components/SummaryPanel/RpsChart.tsx
+++ b/frontend/src/components/SummaryPanel/RpsChart.tsx
@@ -61,7 +61,7 @@ const renderSparklines = (series: VCLines<RichDataPoint>, yTickFormat?: (val: nu
       width={SUMMARY_PANEL_CHART_WIDTH}
       showLegend={false}
       padding={{ top: 5 }}
-      tooltipFormat={dp => `${(dp.x as Date).toLocaleTimeString()}\n${yFormat(dp.y)}`}
+      tooltipFormat={dp => `${(dp.x as Date).toLocaleStringWithConditionalDate()}\n${yFormat(dp.y)}`}
       series={series}
       labelName="ops"
     />
diff --git a/frontend/src/components/Tab/Tabs.tsx b/frontend/src/components/Tab/Tabs.tsx
index ad69d2a4f..73bf85f84 100644
--- a/frontend/src/components/Tab/Tabs.tsx
+++ b/frontend/src/components/Tab/Tabs.tsx
@@ -10,7 +10,7 @@ type TabsProps = {
   onSelect: (tabName: string) => void;
   postHandler?: (tabName: string) => void;
   tabMap: { [key: string]: number };
-  tabName: string;
+  tabName?: string;
   unmountOnExit?: boolean;
 };
 
@@ -70,9 +70,12 @@ export default class ParameterizedTabs extends React.Component<TabsProps> {
 
   tabSelectHandler = (tabKey: string) => {
     const urlParams = new URLSearchParams('');
-    urlParams.set(this.props.tabName, tabKey);
 
-    history.push(history.location.pathname + '?' + urlParams.toString());
+    if (!!this.props.tabName) {
+      urlParams.set(this.props.tabName, tabKey);
+
+      history.push(history.location.pathname + '?' + urlParams.toString());
+    }
 
     if (this.props.postHandler) {
       this.props.postHandler(tabKey);
diff --git a/frontend/src/components/Time/Replay.tsx b/frontend/src/components/Time/Replay.tsx
index a181a060a..57aa753e0 100644
--- a/frontend/src/components/Time/Replay.tsx
+++ b/frontend/src/components/Time/Replay.tsx
@@ -285,7 +285,12 @@ export class Replay extends React.PureComponent<ReplayProps, ReplayState> {
           <span className={controlStyle}>
             {this.state.status === 'playing' ? (
               <Tooltip key="replay-pause" position="top" content="Pause" entryDelay={1000}>
-                <Button data-test="graph-replay-pause-button" className={controlButtonStyle} variant={ButtonVariant.link} onClick={this.pause}>
+                <Button
+                  data-test="graph-replay-pause-button"
+                  className={controlButtonStyle}
+                  variant={ButtonVariant.link}
+                  onClick={this.pause}
+                >
                   <KialiIcon.PauseCircle className={controlIconStyle} />
                 </Button>
               </Tooltip>
@@ -296,7 +301,12 @@ export class Replay extends React.PureComponent<ReplayProps, ReplayState> {
                 content={this.state.status === 'done' ? 'Play again' : 'Play'}
                 entryDelay={1000}
               >
-                <Button data-test="graph-replay-play-button" className={controlButtonStyle} variant={ButtonVariant.link} onClick={this.play}>
+                <Button
+                  data-test="graph-replay-play-button"
+                  className={controlButtonStyle}
+                  variant={ButtonVariant.link}
+                  onClick={this.play}
+                >
                   <KialiIcon.PlayCircle className={controlIconStyle} />
                 </Button>
               </Tooltip>
diff --git a/frontend/src/components/Time/TimeDurationComponent.tsx b/frontend/src/components/Time/TimeDurationComponent.tsx
index 1fe3621a7..c623ede31 100644
--- a/frontend/src/components/Time/TimeDurationComponent.tsx
+++ b/frontend/src/components/Time/TimeDurationComponent.tsx
@@ -52,12 +52,7 @@ export class TimeDurationComponent extends React.PureComponent<TimeControlsProps
           tooltipPosition={TooltipPosition.left}
         />
         {!(this.props.supportsReplay && this.props.replayActive) && (
-          <RefreshContainer
-            id="time_range_refresh"
-            disabled={this.props.disabled}
-            hideLabel={true}
-            manageURL={true}
-          />
+          <RefreshContainer id="time_range_refresh" disabled={this.props.disabled} hideLabel={true} manageURL={true} />
         )}
         {this.props.supportsReplay && this.props.replayActive && (
           <Button
diff --git a/frontend/src/components/Time/TimeDurationIndicatorComponent.tsx b/frontend/src/components/Time/TimeDurationIndicatorComponent.tsx
index 99ed296a1..567dc9629 100644
--- a/frontend/src/components/Time/TimeDurationIndicatorComponent.tsx
+++ b/frontend/src/components/Time/TimeDurationIndicatorComponent.tsx
@@ -1,17 +1,17 @@
-import * as React from "react";
-import { style } from "typestyle";
-import { Button, Tooltip } from "@patternfly/react-core";
-import { config } from "../../config";
-import { KialiIcon } from "../../config/KialiIcon";
-import { DurationInSeconds, guardTimeRange, TimeRange } from "../../types/Common";
-import { getName, getRefreshIntervalName } from "../../utils/RateIntervals";
-import { KialiAppState } from "../../store/Store";
-import { durationSelector, refreshIntervalSelector, timeRangeSelector } from "../../store/Selectors";
-import { connect } from "react-redux";
-import history, { HistoryManager } from "../../app/History";
-import { KialiDispatch } from "../../types/Redux";
-import { bindActionCreators } from "redux";
-import { UserSettingsActions } from "../../actions/UserSettingsActions";
+import * as React from 'react';
+import { style } from 'typestyle';
+import { Button, Tooltip } from '@patternfly/react-core';
+import { config } from '../../config';
+import { KialiIcon } from '../../config/KialiIcon';
+import { DurationInSeconds, guardTimeRange, TimeRange } from '../../types/Common';
+import { getName, getRefreshIntervalName } from '../../utils/RateIntervals';
+import { KialiAppState } from '../../store/Store';
+import { durationSelector, refreshIntervalSelector, timeRangeSelector } from '../../store/Selectors';
+import { connect } from 'react-redux';
+import history, { HistoryManager } from '../../app/History';
+import { KialiDispatch } from '../../types/Redux';
+import { bindActionCreators } from 'redux';
+import { UserSettingsActions } from '../../actions/UserSettingsActions';
 
 interface Props {
   isDuration?: boolean;
@@ -55,33 +55,38 @@ export class TimeDurationIndicatorComponent extends React.PureComponent<Props> {
 
   timeDurationDetail() {
     if (this.props.isDuration) {
-      return `Last ${getName(this.props.duration)}`
+      return `Last ${getName(this.props.duration)}`;
     } else {
       return guardTimeRange(
         this.props.timeRange,
-        (d) => `Last ${getName(d)}`,
-        (b) => (new Date(b.from!).toLocaleString() + ' to ' + (b.to ? new Date(b.to).toLocaleString() : 'now')))
+        d => `Last ${getName(d)}`,
+        b => new Date(b.from!).toLocaleString() + ' to ' + (b.to ? new Date(b.to).toLocaleString() : 'now')
+      );
     }
   }
 
-  render () {
+  render() {
     return (
-      <Tooltip isContentLeftAligned={true} maxWidth={'50em'} content={
-        <>
-        <p>Select the time range of shown data, and the refresh interval.</p>
-      <p style={{whiteSpace: 'nowrap'}}>
-      {this.timeDurationDetailLabel()}: {this.timeDurationDetail()}
-      <br />
-      Current refresh interval: {config.toolbar.refreshInterval[this.props.refreshInterval]}
-      </p>
-      </>
-    }>
-      <Button variant="link" isInline={true} onClick={this.props.onClick}>
-      <KialiIcon.Clock className={infoStyle} />
-      {this.timeDurationIndicator()}, {getRefreshIntervalName(this.props.refreshInterval)}
-      </Button>
+      <Tooltip
+        isContentLeftAligned={true}
+        maxWidth={'50em'}
+        content={
+          <>
+            <p>Select the time range of shown data, and the refresh interval.</p>
+            <p style={{ whiteSpace: 'nowrap' }}>
+              {this.timeDurationDetailLabel()}: {this.timeDurationDetail()}
+              <br />
+              Current refresh interval: {config.toolbar.refreshInterval[this.props.refreshInterval]}
+            </p>
+          </>
+        }
+      >
+        <Button variant="link" isInline={true} onClick={this.props.onClick}>
+          <KialiIcon.Clock className={infoStyle} />
+          {this.timeDurationIndicator()}, {getRefreshIntervalName(this.props.refreshInterval)}
+        </Button>
       </Tooltip>
-    )
+    );
   }
 }
 
@@ -97,9 +102,6 @@ const mapDispatchToProps = (dispatch: KialiDispatch) => {
   };
 };
 
-const TimeDurationIndicatorContainer = connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(TimeDurationIndicatorComponent);
+const TimeDurationIndicatorContainer = connect(mapStateToProps, mapDispatchToProps)(TimeDurationIndicatorComponent);
 
 export default TimeDurationIndicatorContainer;
diff --git a/frontend/src/components/Time/TimeDurationModal.tsx b/frontend/src/components/Time/TimeDurationModal.tsx
index b42dffe18..a33f1f485 100644
--- a/frontend/src/components/Time/TimeDurationModal.tsx
+++ b/frontend/src/components/Time/TimeDurationModal.tsx
@@ -1,13 +1,13 @@
-import * as React from "react";
-import { Button, Form, FormGroup, Modal, ModalVariant, TooltipPosition } from "@patternfly/react-core";
-import { UserSettingsActions } from "../../actions/UserSettingsActions";
-import { HistoryManager, URLParam } from "../../app/History";
-import { useKialiDispatch, useKialiSelector } from "../../hooks/redux";
-import { DurationInSeconds, IntervalInMilliseconds, TimeRange } from "../../types/Common";
-import { DurationDropdownComponent } from "../DurationDropdown/DurationDropdown";
-import { Refresh } from "../Refresh/Refresh";
-import { TimeRangeComponent } from "./TimeRangeComponent";
-import {kioskDurationAction, kioskTimeRangeAction} from "../Kiosk/KioskActions";
+import * as React from 'react';
+import { Button, Form, FormGroup, Modal, ModalVariant, TooltipPosition } from '@patternfly/react-core';
+import { UserSettingsActions } from '../../actions/UserSettingsActions';
+import { HistoryManager, URLParam } from '../../app/History';
+import { useKialiDispatch, useKialiSelector } from '../../hooks/redux';
+import { DurationInSeconds, IntervalInMilliseconds, TimeRange } from '../../types/Common';
+import { DurationDropdownComponent } from '../DurationDropdown/DurationDropdown';
+import { Refresh } from '../Refresh/Refresh';
+import { TimeRangeComponent } from './TimeRangeComponent';
+import { kioskDurationAction, kioskTimeRangeAction } from '../Kiosk/KioskActions';
 
 interface Props {
   customDuration: boolean;
@@ -19,9 +19,9 @@ interface Props {
 
 export function TimeDurationModal(props: Props) {
   const dispatch = useKialiDispatch();
-  const reduxDuration = useKialiSelector((state) => state.userSettings.duration);
-  const reduxRefreshInterval = useKialiSelector((state) => state.userSettings.refreshInterval);
-  const reduxTimeRange = useKialiSelector((state) => state.userSettings.timeRange);
+  const reduxDuration = useKialiSelector(state => state.userSettings.duration);
+  const reduxRefreshInterval = useKialiSelector(state => state.userSettings.refreshInterval);
+  const reduxTimeRange = useKialiSelector(state => state.userSettings.timeRange);
 
   const [duration, setDuration] = React.useState(reduxDuration);
   const [refreshInterval, setRefreshInterval] = React.useState(reduxRefreshInterval);
@@ -73,24 +73,30 @@ export function TimeDurationModal(props: Props) {
       width={700}
       isOpen={props.isOpen}
       showClose={false}
-      actions={[<Button key="confirm" variant="primary" onClick={handleConfirm}>Confirm</Button>,<Button key="cancel" variant="link" onClick={handleCancel}>Cancel</Button>]}
+      actions={[
+        <Button key="confirm" variant="primary" onClick={handleConfirm}>
+          Confirm
+        </Button>,
+        <Button key="cancel" variant="link" onClick={handleCancel}>
+          Cancel
+        </Button>
+      ]}
       position="top"
     >
       <Form isHorizontal={true}>
         {props.customDuration ? (
-          <FormGroup
-            label="Time range"
-            fieldId="drform-time-range"
-          >
-            <div style={{display: "flex"}}>
-              <TimeRangeComponent menuAppendTo="parent" timeRange={timeRange} setTimeRange={handleSetTimeRange} tooltip={'Time range'} />
+          <FormGroup label="Time range" fieldId="drform-time-range">
+            <div style={{ display: 'flex' }}>
+              <TimeRangeComponent
+                menuAppendTo="parent"
+                timeRange={timeRange}
+                setTimeRange={handleSetTimeRange}
+                tooltip={'Time range'}
+              />
             </div>
           </FormGroup>
         ) : (
-          <FormGroup
-            label="Duration"
-            fieldId="drform-duration"
-          >
+          <FormGroup label="Duration" fieldId="drform-duration">
             <DurationDropdownComponent
               id={'drform-duration-dd'}
               disabled={false}
@@ -103,10 +109,7 @@ export function TimeDurationModal(props: Props) {
             />
           </FormGroup>
         )}
-        <FormGroup
-          label="Refresh interval"
-          fieldId="drform-refresh"
-        >
+        <FormGroup label="Refresh interval" fieldId="drform-refresh">
           <Refresh
             id="drform-metrics-refresh"
             hideLabel={true}
diff --git a/frontend/src/components/ToolbarDropdown/ToolbarDropdown.tsx b/frontend/src/components/ToolbarDropdown/ToolbarDropdown.tsx
index 8ff440d89..fe0744250 100644
--- a/frontend/src/components/ToolbarDropdown/ToolbarDropdown.tsx
+++ b/frontend/src/components/ToolbarDropdown/ToolbarDropdown.tsx
@@ -7,10 +7,10 @@ const widthAuto = style({
 });
 
 const dropdownTitle = style({
-  fontSize: 'var(--pf-global--FontSize--md)',  // valueOf --pf-c-select__toggle--FontSize
+  fontSize: 'var(--pf-global--FontSize--md)', // valueOf --pf-c-select__toggle--FontSize
   // @ts-ignore
   fontWeight: 'var(--pf-global--FontSize--normal)', // valueOf --pf-c-select__toggle--FontWeigt
-  marginRight: '10px',
+  marginRight: '10px'
 });
 
 type ToolbarDropdownProps = {
diff --git a/frontend/src/components/TrafficList/TrafficDetails.tsx b/frontend/src/components/TrafficList/TrafficDetails.tsx
index efc408a6a..6adfdc94d 100644
--- a/frontend/src/components/TrafficList/TrafficDetails.tsx
+++ b/frontend/src/components/TrafficList/TrafficDetails.tsx
@@ -20,10 +20,10 @@ import { KialiAppState } from '../../store/Store';
 import { connect } from 'react-redux';
 import { durationSelector } from '../../store/Selectors';
 import { HealthAnnotationType } from '../../types/HealthAnnotation';
-import TrafficListComponentContainer from "components/TrafficList/TrafficListComponent";
-import { KioskElement } from "../Kiosk/KioskElement";
-import { TimeDurationModal } from "../Time/TimeDurationModal";
-import TimeDurationIndicatorContainer from "../Time/TimeDurationIndicatorComponent";
+import TrafficListComponentContainer from 'components/TrafficList/TrafficListComponent';
+import { KioskElement } from '../Kiosk/KioskElement';
+import { TimeDurationModal } from '../Time/TimeDurationModal';
+import TimeDurationIndicatorContainer from '../Time/TimeDurationIndicatorComponent';
 
 export interface AppNode {
   id: string;
@@ -68,7 +68,7 @@ export interface TrafficItem {
 
 type ReduxProps = {
   duration: DurationInSeconds;
-}
+};
 
 type TrafficDetailsProps = ReduxProps & {
   itemName: string;
@@ -128,7 +128,10 @@ class TrafficDetails extends React.Component<TrafficDetailsProps, TrafficDetails
                     <ToolbarGroup>
                       <KioskElement>
                         <ToolbarItem style={{ marginLeft: 'auto' }}>
-                          <TimeDurationIndicatorContainer isDuration={true} onClick={this.toggleTimeOptionsVisibility} />
+                          <TimeDurationIndicatorContainer
+                            isDuration={true}
+                            onClick={this.toggleTimeOptionsVisibility}
+                          />
                         </ToolbarItem>
                       </KioskElement>
                     </ToolbarGroup>
@@ -147,7 +150,8 @@ class TrafficDetails extends React.Component<TrafficDetailsProps, TrafficDetails
           customDuration={false}
           isOpen={this.state.isTimeOptionsOpen}
           onConfirm={this.toggleTimeOptionsVisibility}
-          onCancel={this.toggleTimeOptionsVisibility} />
+          onCancel={this.toggleTimeOptionsVisibility}
+        />
       </>
     );
   }
@@ -326,13 +330,13 @@ class TrafficDetails extends React.Component<TrafficDetailsProps, TrafficDetails
   };
 
   private toggleTimeOptionsVisibility = () => {
-    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }) );
-  }
+    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }));
+  };
 }
 
 const mapStateToProps = (state: KialiAppState) => {
   return {
-    duration: durationSelector(state),
+    duration: durationSelector(state)
   };
 };
 
diff --git a/frontend/src/components/TrafficList/TrafficListComponent.tsx b/frontend/src/components/TrafficList/TrafficListComponent.tsx
index 7bb0c51a2..9e7934624 100644
--- a/frontend/src/components/TrafficList/TrafficListComponent.tsx
+++ b/frontend/src/components/TrafficList/TrafficListComponent.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import {Title, TitleSizes, Tooltip, TooltipPosition} from '@patternfly/react-core';
+import { Title, TitleSizes, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { style } from 'typestyle';
 import { IRow, sortable, SortByDirection, Table, TableBody, TableHeader, cellWidth } from '@patternfly/react-table';
 import { Link } from 'react-router-dom';
@@ -14,9 +14,9 @@ import { sortFields } from './FiltersAndSorts';
 import { SortField } from 'types/SortFilters';
 import { PFBadgeType, PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { createTooltipIcon, KialiIcon } from 'config/KialiIcon';
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
-import {isParentKiosk, kioskContextMenuAction} from "../Kiosk/KioskActions";
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
+import { isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions';
 
 export interface TrafficListItem {
   direction: TrafficDirection;
@@ -31,12 +31,13 @@ export interface TrafficListItem {
 
 type ReduxProps = {
   kiosk: string;
-}
-
-type TrafficListComponentProps = ReduxProps & FilterComponent.Props<TrafficListItem> & {
-  trafficItems: TrafficItem[];
 };
 
+type TrafficListComponentProps = ReduxProps &
+  FilterComponent.Props<TrafficListItem> & {
+    trafficItems: TrafficItem[];
+  };
+
 type TrafficListComponentState = FilterComponent.State<TrafficListItem>;
 
 const columns = [
@@ -66,19 +67,16 @@ const columns = [
 ];
 
 function LockIcon(props) {
-  const msg = props.mTLS ? props.mTLS + " % of mTLS traffic" : "mTLS is disabled";
+  const msg = props.mTLS ? props.mTLS + ' % of mTLS traffic' : 'mTLS is disabled';
   return (
-    <Tooltip
-      position={TooltipPosition.top}
-      content={msg}
-    >
+    <Tooltip position={TooltipPosition.top} content={msg}>
       <>
-      {props.mTLS && (<KialiIcon.MtlsLock className={lockIconStyle}/>)}
-      {!props.mTLS && (<KialiIcon.MtlsUnlock className={lockIconStyle}/>)}
+        {props.mTLS && <KialiIcon.MtlsLock className={lockIconStyle} />}
+        {!props.mTLS && <KialiIcon.MtlsUnlock className={lockIconStyle} />}
       </>
     </Tooltip>
   );
-};
+}
 
 // Style constants
 const containerPadding = style({ padding: '20px' });
@@ -259,29 +257,39 @@ class TrafficListComponent extends FilterComponent.Component<
             </>,
             <>
               <PFBadge badge={item.badge} position={TooltipPosition.top} keyValue={`tt_badge_${i}`} />
-              {!!links.detail ? (parentKiosk ? (
-                <Link
-                  key={`link_d_${item.badge}_${name}`}
-                  to={''}
-                  onClick={() => {
-                    kioskContextMenuAction(links.detail);
-                  }}
-                  className={'virtualitem_definition_link'}>
-                  {name}
-                </Link>
-              ) :(
-                <Link key={`link_d_${item.badge}_${name}`} to={links.detail} className={'virtualitem_definition_link'}>
-                  {name}
-                </Link>
-              )) : (
+              {!!links.detail ? (
+                parentKiosk ? (
+                  <Link
+                    key={`link_d_${item.badge}_${name}`}
+                    to={''}
+                    onClick={() => {
+                      kioskContextMenuAction(links.detail);
+                    }}
+                    className={'virtualitem_definition_link'}
+                  >
+                    {name}
+                  </Link>
+                ) : (
+                  <Link
+                    key={`link_d_${item.badge}_${name}`}
+                    to={links.detail}
+                    className={'virtualitem_definition_link'}
+                  >
+                    {name}
+                  </Link>
+                )
+              ) : (
                 name
               )}
             </>,
             <>{item.trafficRate}</>,
             <>{item.trafficPercentSuccess}</>,
-            <>{item.protocol}<LockIcon mTLS={item.mTLS}></LockIcon></>,
             <>
-              {!!links.metrics && (
+              {item.protocol}
+              <LockIcon mTLS={item.mTLS}></LockIcon>
+            </>,
+            <>
+              {!!links.metrics &&
                 (parentKiosk ? (
                   <Link
                     key={`link_m_${item.badge}_${name}`}
@@ -294,11 +302,14 @@ class TrafficListComponent extends FilterComponent.Component<
                     View metrics
                   </Link>
                 ) : (
-                    <Link key={`link_m_${item.badge}_${name}`} to={links.metrics} className={'virtualitem_definition_link'}>
-                      View metrics
-                    </Link>
-                  ))
-              )}
+                  <Link
+                    key={`link_m_${item.badge}_${name}`}
+                    to={links.metrics}
+                    className={'virtualitem_definition_link'}
+                  >
+                    View metrics
+                  </Link>
+                ))}
             </>
           ]
         };
@@ -371,9 +382,9 @@ class TrafficListComponent extends FilterComponent.Component<
 
 const mapStateToProps = (state: KialiAppState) => {
   return {
-    kiosk: state.globalState.kiosk,
+    kiosk: state.globalState.kiosk
   };
 };
 
-const TrafficListComponentContainer = connect(mapStateToProps)(TrafficListComponent)
+const TrafficListComponentContainer = connect(mapStateToProps)(TrafficListComponent);
 export default TrafficListComponentContainer;
diff --git a/frontend/src/components/VirtualList/Config.ts b/frontend/src/components/VirtualList/Config.ts
index e8be59ac7..e6eebbb3e 100644
--- a/frontend/src/components/VirtualList/Config.ts
+++ b/frontend/src/components/VirtualList/Config.ts
@@ -12,7 +12,7 @@ import NamespaceInfo from '../../pages/Overview/NamespaceInfo';
 import * as React from 'react';
 import { StatefulFilters } from '../Filters/StatefulFilters';
 import { PFBadges, PFBadgeType } from '../../components/Pf/PfBadges';
-import { isGateway } from "../../helpers/LabelFilterHelper";
+import { isGateway } from '../../helpers/LabelFilterHelper';
 
 export type SortResource = AppListItem | WorkloadListItem | ServiceListItem;
 export type TResource = SortResource | IstioConfigItem;
@@ -222,8 +222,8 @@ export const IstioTypes = {
   workloadentry: { name: 'WorkloadEntry', url: 'workloadentries', badge: PFBadges.WorkloadEntry } as istioConfigType,
   workloadgroup: { name: 'WorkloadGroup', url: 'workloadgroups', badge: PFBadges.WorkloadGroup } as istioConfigType,
   envoyfilter: { name: 'EnvoyFilter', url: 'envoyfilters', badge: PFBadges.EnvoyFilter } as istioConfigType,
-  wasmplugin: {name: 'WasmPlugin', url: 'wasmplugins', badge: PFBadges.WasmPlugin} as istioConfigType,
-  telemetry: {name: 'Telemetry', url: 'telemetries', badge: PFBadges.Telemetry} as istioConfigType,
+  wasmplugin: { name: 'WasmPlugin', url: 'wasmplugins', badge: PFBadges.WasmPlugin } as istioConfigType,
+  telemetry: { name: 'Telemetry', url: 'telemetries', badge: PFBadges.Telemetry } as istioConfigType,
   attributemanifest: {
     name: 'AttributeManifest',
     url: 'attributemanifests',
diff --git a/frontend/src/components/VirtualList/Renderers.tsx b/frontend/src/components/VirtualList/Renderers.tsx
index 60db220bf..59d21f15b 100644
--- a/frontend/src/components/VirtualList/Renderers.tsx
+++ b/frontend/src/components/VirtualList/Renderers.tsx
@@ -34,7 +34,7 @@ import Label from 'components/Label/Label';
 import { serverConfig } from 'config/ServerConfig';
 import ControlPlaneBadge from 'pages/Overview/ControlPlaneBadge';
 import NamespaceStatuses from 'pages/Overview/NamespaceStatuses';
-import { isGateway } from "../../helpers/LabelFilterHelper";
+import { isGateway } from '../../helpers/LabelFilterHelper';
 
 // Links
 
@@ -84,7 +84,7 @@ export const details: Renderer<AppListItem | WorkloadListItem | ServiceListItem>
         )}
         {hasMissingSC && (
           <li>
-            <MissingSidecar namespace={item.namespace} isGateway={isGateway(item.labels)}/>
+            <MissingSidecar namespace={item.namespace} isGateway={isGateway(item.labels)} />
           </li>
         )}
         {isWorkload && (hasMissingApp || hasMissingVersion) && (
@@ -151,9 +151,14 @@ export const status: Renderer<NamespaceInfo> = (ns: NamespaceInfo) => {
         className="pf-m-center"
         style={{ verticalAlign: 'middle' }}
       >
-        { ns.status &&
-          <NamespaceStatuses key={ns.name} name={ns.name} status={ns.status} type={OverviewToolbar.currentOverviewType()} />
-        }
+        {ns.status && (
+          <NamespaceStatuses
+            key={ns.name}
+            name={ns.name}
+            status={ns.status}
+            type={OverviewToolbar.currentOverviewType()}
+          />
+        )}
         <OverviewCardSparklineCharts
           key={ns.name}
           name={ns.name}
@@ -174,9 +179,7 @@ export const nsItem: Renderer<NamespaceInfo> = (ns: NamespaceInfo, _config: Reso
     <td role="gridcell" key={'VirtuaItem_NamespaceItem_' + ns.name} style={{ verticalAlign: 'middle' }}>
       <PFBadge badge={badge} />
       {ns.name}
-      {ns.name === serverConfig.istioNamespace &&
-        <ControlPlaneBadge />
-      }
+      {ns.name === serverConfig.istioNamespace && <ControlPlaneBadge />}
     </td>
   );
 };
diff --git a/frontend/src/config/ServerConfig.ts b/frontend/src/config/ServerConfig.ts
index 5f7ff28e9..cf9d3fe89 100644
--- a/frontend/src/config/ServerConfig.ts
+++ b/frontend/src/config/ServerConfig.ts
@@ -50,9 +50,12 @@ const computeValidDurations = (cfg: ComputedServerConfig) => {
 // Set some reasonable defaults. Initial values should be valid for fields
 // than may not be providedby/set on the server.
 const defaultServerConfig: ComputedServerConfig = {
+  accessibleNamespaces: [],
+  authStrategy: '',
   clusters: {},
   durations: {},
   gatewayAPIEnabled: false,
+  logLevel: '',
   healthConfig: {
     rate: []
   },
diff --git a/frontend/src/helpers/EnvoyHelpers.tsx b/frontend/src/helpers/EnvoyHelpers.tsx
index cafcd95f5..7db6dbdc9 100644
--- a/frontend/src/helpers/EnvoyHelpers.tsx
+++ b/frontend/src/helpers/EnvoyHelpers.tsx
@@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
 import { EnvoySummary, Host } from '../types/IstioObjects';
 import { ActiveFilter, ActiveFiltersInfo } from '../types/Filters';
 import { FilterSelected } from '../components/Filters/StatefulFilters';
-import {kioskContextMenuAction} from "../components/Kiosk/KioskActions";
+import { kioskContextMenuAction } from '../components/Kiosk/KioskActions';
 
 export type FilterMethodMap = { [id: string]: (value, filter) => boolean };
 
@@ -28,7 +28,7 @@ export const routeLink = (
   route: string,
   namespace: string,
   workload: string | undefined,
-  handler: () => void,
+  handler: () => void
 ): JSX.Element | string => {
   let re = /Route: ([a-z-.:\d]*)/;
 
@@ -53,7 +53,7 @@ export const serviceLink = (
   namespaces: Namespace[] | undefined,
   podNamespace: string,
   simpleSvc: boolean = false,
-  isParentKiosk: boolean,
+  isParentKiosk: boolean
 ): JSX.Element | string => {
   let to: string = '/namespaces/';
   let linkText: string = host.service;
@@ -84,7 +84,9 @@ export const serviceLink = (
             onClick={() => {
               kioskContextMenuAction(to);
             }}
-          >{linkText}</Link>
+          >
+            {linkText}
+          </Link>
         ) : (
           <Link to={to}>{linkText}</Link>
         )}
diff --git a/frontend/src/helpers/LabelFilterHelper.ts b/frontend/src/helpers/LabelFilterHelper.ts
index 32d327976..752fc1849 100644
--- a/frontend/src/helpers/LabelFilterHelper.ts
+++ b/frontend/src/helpers/LabelFilterHelper.ts
@@ -100,5 +100,5 @@ const getKeyAndValues = (filters: string[]): { keys: string[]; keyValues: string
 };
 
 export const isGateway = (labels: { [key: string]: string }): boolean => {
-  return labels && 'istio' in labels && (labels['istio'] === 'ingressgateway' || labels['istio'] === 'egressgateway')
+  return labels && 'istio' in labels && (labels['istio'] === 'ingressgateway' || labels['istio'] === 'egressgateway');
 };
diff --git a/frontend/src/helpers/__tests__/LabelFilterHelper.test.ts b/frontend/src/helpers/__tests__/LabelFilterHelper.test.ts
index b84bad10e..76fabfccc 100644
--- a/frontend/src/helpers/__tests__/LabelFilterHelper.test.ts
+++ b/frontend/src/helpers/__tests__/LabelFilterHelper.test.ts
@@ -293,17 +293,17 @@ describe('LabelFilter', () => {
   });
 
   it('check is Ingress/Egress Gateway when false', () => {
-    const result = isGateway({'istio': 'wrong'});
+    const result = isGateway({ istio: 'wrong' });
     expect(result).toBeFalsy();
   });
 
   it('check is Ingress Gateway when true', () => {
-    const result = isGateway({'istio': 'ingressgateway'});
+    const result = isGateway({ istio: 'ingressgateway' });
     expect(result).toBeTruthy();
   });
 
   it('check is Egress Gateway when true', () => {
-    const result = isGateway({'istio': 'egressgateway'});
+    const result = isGateway({ istio: 'egressgateway' });
     expect(result).toBeTruthy();
   });
 });
diff --git a/frontend/src/hooks/redux.ts b/frontend/src/hooks/redux.ts
index 658b960cd..3472c46b6 100644
--- a/frontend/src/hooks/redux.ts
+++ b/frontend/src/hooks/redux.ts
@@ -1,11 +1,14 @@
-import { useDispatch, useSelector } from "react-redux";
-import { KialiAppState } from "../store/Store";
-import { KialiDispatch } from "../types/Redux";
+import { useDispatch, useSelector } from 'react-redux';
+import { KialiAppState } from '../store/Store';
+import { KialiDispatch } from '../types/Redux';
 
 export function useKialiDispatch() {
   return useDispatch<KialiDispatch>();
 }
 
-export function useKialiSelector<TSelected = unknown>(selector: (state: KialiAppState) => TSelected, equalityFn?: (left: TSelected, right: TSelected) => boolean) {
+export function useKialiSelector<TSelected = unknown>(
+  selector: (state: KialiAppState) => TSelected,
+  equalityFn?: (left: TSelected, right: TSelected) => boolean
+) {
   return useSelector<KialiAppState, TSelected>(selector, equalityFn);
 }
diff --git a/frontend/src/hooks/refresh.ts b/frontend/src/hooks/refresh.ts
index a53e85bb0..57aaa45dd 100644
--- a/frontend/src/hooks/refresh.ts
+++ b/frontend/src/hooks/refresh.ts
@@ -1,13 +1,13 @@
-import { useSelector } from "react-redux";
-import { KialiAppState } from "../store/Store";
-import { IntervalInMilliseconds, TimeInMilliseconds } from "../types/Common";
-import { useEffect, useState } from "react";
+import { useSelector } from 'react-redux';
+import { KialiAppState } from '../store/Store';
+import { IntervalInMilliseconds, TimeInMilliseconds } from '../types/Common';
+import { useEffect, useState } from 'react';
 
 let numSubscribers = 0;
 let intervalId: null | number = null;
 
 function doTick(time?: TimeInMilliseconds) {
-  const refreshTick = new CustomEvent('refreshTick', {detail: time ?? Date.now()});
+  const refreshTick = new CustomEvent('refreshTick', { detail: time ?? Date.now() });
   document.dispatchEvent(refreshTick);
 }
 
@@ -16,7 +16,9 @@ export function triggerRefresh(time?: TimeInMilliseconds) {
 }
 
 export default function useRefreshInterval() {
-  const refreshInterval = useSelector<KialiAppState, IntervalInMilliseconds>((state) => state.userSettings.refreshInterval);
+  const refreshInterval = useSelector<KialiAppState, IntervalInMilliseconds>(
+    state => state.userSettings.refreshInterval
+  );
   const [lastRefreshAt, setLastRefreshAt] = useState<TimeInMilliseconds>(Date.now());
   const [previousRefreshAt, setPreviousRefreshAt] = useState<TimeInMilliseconds>(lastRefreshAt);
 
@@ -54,7 +56,7 @@ export default function useRefreshInterval() {
         window.clearInterval(intervalId);
         intervalId = null;
       }
-    }
+    };
   }, [refreshInterval]);
 
   return { lastRefreshAt, previousRefreshAt, refreshInterval };
diff --git a/frontend/src/hooks/services.ts b/frontend/src/hooks/services.ts
index 6a619b5b0..e65638bc1 100644
--- a/frontend/src/hooks/services.ts
+++ b/frontend/src/hooks/services.ts
@@ -1,15 +1,20 @@
-import * as React from "react";
-import { CancelablePromise } from "../utils/CancelablePromises";
-import * as API from "../services/Api";
-import { DurationInSeconds, TimeInMilliseconds } from "../types/Common";
-import { ServiceDetailsInfo } from "../types/ServiceInfo";
-import { getGatewaysAsList, PeerAuthentication } from "../types/IstioObjects";
-import { AxiosError } from "axios";
-import { DecoratedGraphNodeData, NodeType } from "../types/Graph";
-import * as AlertUtils from "../utils/AlertUtils";
-import { useState } from "react";
+import * as React from 'react';
+import { CancelablePromise } from '../utils/CancelablePromises';
+import * as API from '../services/Api';
+import { DurationInSeconds, TimeInMilliseconds } from '../types/Common';
+import { ServiceDetailsInfo } from '../types/ServiceInfo';
+import { getGatewaysAsList, PeerAuthentication } from '../types/IstioObjects';
+import { AxiosError } from 'axios';
+import { DecoratedGraphNodeData, NodeType } from '../types/Graph';
+import * as AlertUtils from '../utils/AlertUtils';
+import { useState } from 'react';
 
-export function useServiceDetail(namespace: string, serviceName: string, duration?: DurationInSeconds, updateTime?: TimeInMilliseconds) {
+export function useServiceDetail(
+  namespace: string,
+  serviceName: string,
+  duration?: DurationInSeconds,
+  updateTime?: TimeInMilliseconds
+) {
   const [isLoading, setIsLoading] = React.useState<boolean>(false);
   const [fetchError, setFetchError] = React.useState<AxiosError | null>(null);
 
@@ -31,7 +36,12 @@ export function useServiceDetail(namespace: string, serviceName: string, duratio
     allPromise.promise
       .then(results => {
         setServiceDetails(results[0]);
-        setGateways(Object.values(results[1].data).map(nsCfg => getGatewaysAsList(nsCfg.gateways)).flat().sort());
+        setGateways(
+          Object.values(results[1].data)
+            .map(nsCfg => getGatewaysAsList(nsCfg.gateways))
+            .flat()
+            .sort()
+        );
         setPeerAuthentications(results[2].data.peerAuthentications);
         setFetchError(null);
         setIsLoading(false);
@@ -54,24 +64,29 @@ export function useServiceDetail(namespace: string, serviceName: string, duratio
       setServiceDetails(null);
       setGateways(null);
       setPeerAuthentications(null);
-    }
+    };
   }, [namespace, serviceName, duration, updateTime]);
 
   return [serviceDetails, gateways, peerAuthentications, isLoading, fetchError] as const;
 }
 
-export function useServiceDetailForGraphNode(node: DecoratedGraphNodeData, loadFlag: boolean, duration?: DurationInSeconds, updateTime?: TimeInMilliseconds) {
+export function useServiceDetailForGraphNode(
+  node: DecoratedGraphNodeData,
+  loadFlag: boolean,
+  duration?: DurationInSeconds,
+  updateTime?: TimeInMilliseconds
+) {
   const [nodeNamespace, setNodeNamespace] = useState<string>('');
   const [nodeSvcName, setNodeSvcName] = useState<string>('');
   const [usedDuration, setUsedDuration] = useState<DurationInSeconds | undefined>(undefined);
-  const [usedUpdateTime, setUsedUpdateTime] = useState<TimeInMilliseconds|undefined>(undefined);
+  const [usedUpdateTime, setUsedUpdateTime] = useState<TimeInMilliseconds | undefined>(undefined);
 
   React.useEffect(() => {
     if (!loadFlag) {
       return;
     }
 
-    const localSvc = (node.nodeType === NodeType.SERVICE && node.service && !node.isServiceEntry) ? node.service : '';
+    const localSvc = node.nodeType === NodeType.SERVICE && node.service && !node.isServiceEntry ? node.service : '';
 
     setNodeNamespace(node.namespace);
     setNodeSvcName(localSvc);
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 27a3570fb..c6366efb7 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,7 +1,20 @@
-import './polyfills';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import App from './app/App';
 import './styles/index.css';
 
+declare global {
+  interface Date {
+    toLocaleStringWithConditionalDate(): string;
+  }
+}
+
+// eslint-disable-next-line no-extend-native
+Date.prototype.toLocaleStringWithConditionalDate = function () {
+  const nowDate = new Date().toLocaleDateString();
+  const thisDate = this.toLocaleDateString();
+
+  return nowDate === thisDate ? this.toLocaleTimeString() : this.toLocaleString();
+};
+
 ReactDOM.render(<App />, document.getElementById('root') as HTMLElement);
diff --git a/frontend/src/pages/AppDetails/AppDetailsPage.tsx b/frontend/src/pages/AppDetails/AppDetailsPage.tsx
index a47c03669..4463f6c91 100644
--- a/frontend/src/pages/AppDetails/AppDetailsPage.tsx
+++ b/frontend/src/pages/AppDetails/AppDetailsPage.tsx
@@ -20,10 +20,10 @@ import TracesComponent from '../../components/JaegerIntegration/TracesComponent'
 import TrafficDetails from 'components/TrafficList/TrafficDetails';
 import TimeControl from '../../components/Time/TimeControl';
 import { AppHealth } from 'types/Health';
-import RenderHeaderContainer from "../../components/Nav/Page/RenderHeader";
-import {ErrorMsg} from "../../types/ErrorMsg";
-import ErrorSection from "../../components/ErrorSection/ErrorSection";
-import connectRefresh from "../../components/Refresh/connectRefresh";
+import RenderHeaderContainer from '../../components/Nav/Page/RenderHeader';
+import { ErrorMsg } from '../../types/ErrorMsg';
+import ErrorSection from '../../components/ErrorSection/ErrorSection';
+import connectRefresh from '../../components/Refresh/connectRefresh';
 
 type AppDetailsState = {
   app?: App;
@@ -40,9 +40,10 @@ type ReduxProps = {
   timeRange: TimeRange;
 };
 
-type AppDetailsProps = RouteComponentProps<AppId> & ReduxProps & {
-  lastRefreshAt: TimeInMilliseconds;
-};
+type AppDetailsProps = RouteComponentProps<AppId> &
+  ReduxProps & {
+    lastRefreshAt: TimeInMilliseconds;
+  };
 
 const tabName = 'tab';
 const defaultTab = 'info';
@@ -99,9 +100,12 @@ class AppDetails extends React.Component<AppDetailsProps, AppDetailsState> {
         });
       })
       .catch(error => {
-        AlertUtils.addError('Could not fetch App Details.', error)
-        const msg : ErrorMsg = {title: 'No App is selected', description: this.props.match.params.app +" is not found in the mesh"};
-        this.setState({error: msg});
+        AlertUtils.addError('Could not fetch App Details.', error);
+        const msg: ErrorMsg = {
+          title: 'No App is selected',
+          description: this.props.match.params.app + ' is not found in the mesh'
+        };
+        this.setState({ error: msg });
       });
   };
 
@@ -239,10 +243,11 @@ class AppDetails extends React.Component<AppDetailsProps, AppDetailsState> {
     }
     return (
       <>
-        <RenderHeaderContainer location={this.props.location} rightToolbar={<TimeControl customDuration={useCustomTime} />} />
-        {this.state.error && (
-          <ErrorSection error={this.state.error} />
-        )}
+        <RenderHeaderContainer
+          location={this.props.location}
+          rightToolbar={<TimeControl customDuration={useCustomTime} />}
+        />
+        {this.state.error && <ErrorSection error={this.state.error} />}
         {this.state.app && (
           <ParameterizedTabs
             id="basic-tabs"
diff --git a/frontend/src/pages/AppDetails/AppInfo.tsx b/frontend/src/pages/AppDetails/AppInfo.tsx
index d5cfffc55..6d4924030 100644
--- a/frontend/src/pages/AppDetails/AppInfo.tsx
+++ b/frontend/src/pages/AppDetails/AppInfo.tsx
@@ -12,7 +12,7 @@ import { meshWideMTLSEnabledSelector } from '../../store/Selectors';
 import { style } from 'typestyle';
 import { GraphEdgeTapEvent } from '../../components/CytoscapeGraph/CytoscapeGraph';
 import history, { URLParam } from '../../app/History';
-import MiniGraphCardContainer from "../../components/CytoscapeGraph/MiniGraphCard";
+import MiniGraphCardContainer from '../../components/CytoscapeGraph/MiniGraphCard';
 
 type AppInfoProps = {
   app?: App;
diff --git a/frontend/src/pages/AppList/AppListPage.tsx b/frontend/src/pages/AppList/AppListPage.tsx
index bd003a966..dd217612c 100644
--- a/frontend/src/pages/AppList/AppListPage.tsx
+++ b/frontend/src/pages/AppList/AppListPage.tsx
@@ -19,7 +19,7 @@ import * as API from '../../services/Api';
 import * as AppListClass from './AppListClass';
 import VirtualList from '../../components/VirtualList/VirtualList';
 import TimeDurationContainer from '../../components/Time/TimeDurationComponent';
-import RefreshNotifier from "../../components/Refresh/RefreshNotifier";
+import RefreshNotifier from '../../components/Refresh/RefreshNotifier';
 
 type AppListPageState = FilterComponent.State<AppListItem>;
 
@@ -118,11 +118,7 @@ class AppListPageComponent extends FilterComponent.Component<AppListPageProps, A
         <div style={{ backgroundColor: '#fff' }}>
           <DefaultSecondaryMasthead
             rightToolbar={
-              <TimeDurationContainer
-                key={'DurationDropdown'}
-                id="app-list-duration-dropdown"
-                disabled={false}
-              />
+              <TimeDurationContainer key={'DurationDropdown'} id="app-list-duration-dropdown" disabled={false} />
             }
           />
         </div>
diff --git a/frontend/src/pages/Graph/GraphHelpTour.tsx b/frontend/src/pages/Graph/GraphHelpTour.tsx
index e4aad1a29..69538205f 100644
--- a/frontend/src/pages/Graph/GraphHelpTour.tsx
+++ b/frontend/src/pages/Graph/GraphHelpTour.tsx
@@ -14,7 +14,7 @@ export const GraphTourStops: { [name: string]: TourStopInfo } = {
     name: 'Display',
     description:
       'Set edge labeling, node badging, and various display options. Response-time edge labeling, security badging, and traffic animation may affect performance. Response-times reflect the 95th percentile.',
-    position: PopoverPosition.rightStart,
+    position: PopoverPosition.rightStart
   },
   Find: {
     name: 'Find and Hide',
diff --git a/frontend/src/pages/Graph/GraphPage.tsx b/frontend/src/pages/Graph/GraphPage.tsx
index 34d1ce93f..d5f1d96a8 100644
--- a/frontend/src/pages/Graph/GraphPage.tsx
+++ b/frontend/src/pages/Graph/GraphPage.tsx
@@ -65,22 +65,22 @@ import GraphDataSource, { FetchParams, EMPTY_GRAPH_DATA } from '../../services/G
 import { NamespaceActions } from '../../actions/NamespaceAction';
 import GraphThunkActions from '../../actions/GraphThunkActions';
 import { JaegerTrace } from 'types/JaegerInfo';
-import { KialiDispatch } from "types/Redux";
+import { KialiDispatch } from 'types/Redux';
 import { JaegerThunkActions } from 'actions/JaegerThunkActions';
 import GraphTour from 'pages/Graph/GraphHelpTour';
 import { getNextTourStop, TourInfo } from 'components/Tour/TourStop';
 import { EdgeContextMenu } from 'components/CytoscapeGraph/ContextMenu/EdgeContextMenu';
 import * as CytoscapeGraphUtils from '../../components/CytoscapeGraph/CytoscapeGraphUtils';
-import { isParentKiosk, kioskContextMenuAction } from "../../components/Kiosk/KioskActions";
-import ServiceWizard from "components/IstioWizards/ServiceWizard";
-import { ServiceDetailsInfo } from "types/ServiceInfo";
-import { DestinationRuleC, PeerAuthentication } from "types/IstioObjects";
-import { WizardAction, WizardMode } from "components/IstioWizards/WizardActions";
-import ConfirmDeleteTrafficRoutingModal from "components/IstioWizards/ConfirmDeleteTrafficRoutingModal";
-import { deleteServiceTrafficRouting } from "services/Api";
-import { canCreate, canUpdate } from "../../types/Permissions";
-import connectRefresh from "../../components/Refresh/connectRefresh";
-import { triggerRefresh } from "../../hooks/refresh";
+import { isParentKiosk, kioskContextMenuAction } from '../../components/Kiosk/KioskActions';
+import ServiceWizard from 'components/IstioWizards/ServiceWizard';
+import { ServiceDetailsInfo } from 'types/ServiceInfo';
+import { DestinationRuleC, PeerAuthentication } from 'types/IstioObjects';
+import { WizardAction, WizardMode } from 'components/IstioWizards/WizardActions';
+import ConfirmDeleteTrafficRoutingModal from 'components/IstioWizards/ConfirmDeleteTrafficRoutingModal';
+import { deleteServiceTrafficRouting } from 'services/Api';
+import { canCreate, canUpdate } from '../../types/Permissions';
+import connectRefresh from '../../components/Refresh/connectRefresh';
+import { triggerRefresh } from '../../hooks/refresh';
 
 // GraphURLPathProps holds path variable values.  Currently all path variables are relevant only to a node graph
 type GraphURLPathProps = {
@@ -141,11 +141,13 @@ type ReduxProps = {
   toggleIdleNodes: () => void;
   toggleLegend: () => void;
   updateSummary: (event: CytoscapeEvent) => void;
+  istioAPIEnabled: boolean;
 };
 
-export type GraphPageProps = RouteComponentProps<Partial<GraphURLPathProps>> & ReduxProps & {
-  lastRefreshAt: TimeInMilliseconds;
-};
+export type GraphPageProps = RouteComponentProps<Partial<GraphURLPathProps>> &
+  ReduxProps & {
+    lastRefreshAt: TimeInMilliseconds;
+  };
 
 export type GraphData = {
   elements: DecoratedGraphElements;
@@ -169,7 +171,7 @@ type WizardsData = {
   peerAuthentications: PeerAuthentication[];
   namespace: string;
   serviceDetails?: ServiceDetailsInfo;
-}
+};
 
 type GraphPageState = {
   graphData: GraphData;
@@ -477,9 +479,7 @@ export class GraphPage extends React.Component<GraphPageProps, GraphPageState> {
               )}
               {isReady && (
                 <Chip
-                  className={`${graphTimeRange} ${
-                    this.props.replayActive ? replayBackground : whiteBackground
-                  }`}
+                  className={`${graphTimeRange} ${this.props.replayActive ? replayBackground : whiteBackground}`}
                   isReadOnly={true}
                 >
                   {this.props.replayActive && <Badge style={{ marginRight: '4px' }} isRead={true}>{`Replay`}</Badge>}
@@ -538,7 +538,10 @@ export class GraphPage extends React.Component<GraphPageProps, GraphPageState> {
           serviceName={this.state.wizardsData.serviceDetails?.service?.name || ''}
           workloads={this.state.wizardsData.serviceDetails?.workloads || []}
           subServices={this.state.wizardsData.serviceDetails?.subServices || []}
-          createOrUpdate={canCreate(this.state.wizardsData.serviceDetails?.istioPermissions) || canUpdate(this.state.wizardsData.serviceDetails?.istioPermissions)}
+          createOrUpdate={
+            canCreate(this.state.wizardsData.serviceDetails?.istioPermissions) ||
+            canUpdate(this.state.wizardsData.serviceDetails?.istioPermissions)
+          }
           virtualServices={this.state.wizardsData.serviceDetails?.virtualServices || []}
           destinationRules={this.state.wizardsData.serviceDetails?.destinationRules || []}
           gateways={this.state.wizardsData.gateways || []}
@@ -547,6 +550,7 @@ export class GraphPage extends React.Component<GraphPageProps, GraphPageState> {
           peerAuthentications={this.state.wizardsData.peerAuthentications || []}
           tlsStatus={this.state.wizardsData.serviceDetails?.namespaceMTLS}
           onClose={this.handleWizardClose}
+          istioAPIEnabled={this.props.istioAPIEnabled}
         />
         {this.state.showConfirmDeleteTrafficRouting && (
           <ConfirmDeleteTrafficRoutingModal
@@ -554,8 +558,9 @@ export class GraphPage extends React.Component<GraphPageProps, GraphPageState> {
             destinationRules={DestinationRuleC.fromDrArray(this.state.wizardsData.serviceDetails!.destinationRules)}
             virtualServices={this.state.wizardsData.serviceDetails!.virtualServices}
             k8sHTTPRoutes={this.state.wizardsData.serviceDetails!.k8sHTTPRoutes}
-            onCancel={() => this.setState({showConfirmDeleteTrafficRouting: false})}
-            onConfirm={this.handleConfirmDeleteServiceTrafficRouting} />
+            onCancel={() => this.setState({ showConfirmDeleteTrafficRouting: false })}
+            onConfirm={this.handleConfirmDeleteServiceTrafficRouting}
+          />
         )}
       </>
     );
@@ -743,13 +748,20 @@ export class GraphPage extends React.Component<GraphPageProps, GraphPageState> {
     return;
   };
 
-  private handleLaunchWizard = (action: WizardAction, mode: WizardMode, namespace: string, serviceDetails: ServiceDetailsInfo, gateways: string[], peerAuths: PeerAuthentication[]) => {
+  private handleLaunchWizard = (
+    action: WizardAction,
+    mode: WizardMode,
+    namespace: string,
+    serviceDetails: ServiceDetailsInfo,
+    gateways: string[],
+    peerAuths: PeerAuthentication[]
+  ) => {
     this.setState(prevState => ({
       wizardsData: {
         ...prevState.wizardsData,
         showWizard: true,
         wizardType: action,
-        updateMode: mode === "update",
+        updateMode: mode === 'update',
         namespace: namespace,
         serviceDetails: serviceDetails,
         gateways: gateways,
@@ -775,7 +787,7 @@ export class GraphPage extends React.Component<GraphPageProps, GraphPageState> {
         }
       }));
     }
-  }
+  };
 
   private handleDeleteTrafficRouting = (_key: string, serviceDetail: ServiceDetailsInfo) => {
     this.setState(prevState => ({
@@ -789,7 +801,7 @@ export class GraphPage extends React.Component<GraphPageProps, GraphPageState> {
 
   private handleConfirmDeleteServiceTrafficRouting = () => {
     this.setState({
-      showConfirmDeleteTrafficRouting:false
+      showConfirmDeleteTrafficRouting: false
     });
 
     deleteServiceTrafficRouting(this.state.wizardsData!.serviceDetails!)
@@ -888,7 +900,8 @@ const mapStateToProps = (state: KialiAppState) => ({
   showVirtualServices: state.graph.toolbarState.showVirtualServices,
   summaryData: state.graph.summaryData,
   trace: state.jaegerState?.selectedTrace,
-  trafficRates: trafficRatesSelector(state)
+  trafficRates: trafficRatesSelector(state),
+  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled
 });
 
 const mapDispatchToProps = (dispatch: KialiDispatch) => ({
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphFind.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphFind.tsx
index 06b4949de..8bf0c5423 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphFind.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphFind.tsx
@@ -15,7 +15,7 @@ import { style } from 'typestyle';
 import TourStopContainer from 'components/Tour/TourStop';
 import { GraphTourStops } from 'pages/Graph/GraphHelpTour';
 import { TimeInMilliseconds } from 'types/Common';
-import { KialiDispatch } from "types/Redux";
+import { KialiDispatch } from 'types/Redux';
 import { AutoComplete } from 'utils/AutoComplete';
 import { DEGRADED, FAILURE, HEALTHY } from 'types/Health';
 import { GraphFindOptions } from './GraphFindOptions';
@@ -78,7 +78,7 @@ const buttonClearStyle = style({
   width: '20px',
   paddingLeft: '5px',
   paddingRight: '5px',
-  bottom:  '0.5px'
+  bottom: '0.5px'
 });
 
 const operands: string[] = [
@@ -294,11 +294,7 @@ export class GraphFind extends React.Component<GraphFindProps, GraphFindState> {
             <GraphFindOptions kind="find" onSelect={this.updateFindOption} />
             {this.props.findValue && (
               <Tooltip key="ot_clear_find" position="top" content="Clear Find...">
-                <Button
-                  className={buttonClearStyle}
-                  variant={ButtonVariant.control}
-                  onClick={() => this.setFind('')}
-                >
+                <Button className={buttonClearStyle} variant={ButtonVariant.control} onClick={() => this.setFind('')}>
                   <KialiIcon.Close />
                 </Button>
               </Tooltip>
@@ -321,11 +317,7 @@ export class GraphFind extends React.Component<GraphFindProps, GraphFindState> {
             <GraphFindOptions kind="hide" onSelect={this.updateHideOption} />
             {this.props.hideValue && (
               <Tooltip key="ot_clear_hide" position="top" content="Clear Hide...">
-                <Button
-                  className={buttonClearStyle}
-                  variant={ButtonVariant.control}
-                  onClick={() => this.setHide('')}
-                >
+                <Button className={buttonClearStyle} variant={ButtonVariant.control} onClick={() => this.setHide('')}>
                   <KialiIcon.Close />
                 </Button>
               </Tooltip>
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphFindOptions.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphFindOptions.tsx
index e1b539885..ec69bc7ef 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphFindOptions.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphFindOptions.tsx
@@ -18,7 +18,7 @@ const dropdown = style({
   width: '20px',
   paddingLeft: '5px',
   paddingRight: 0,
-  bottom:  '0.5px'
+  bottom: '0.5px'
 });
 
 export class GraphFindOptions extends React.PureComponent<GraphFindOptionsProps, GraphFindOptionsState> {
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphSecondaryMasthead.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphSecondaryMasthead.tsx
index 8598ed4f0..223c3e98f 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphSecondaryMasthead.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphSecondaryMasthead.tsx
@@ -75,11 +75,7 @@ export default class GraphSecondaryMasthead extends React.PureComponent<GraphSec
           </TourStopContainer>
           <div className={rightToolbarStyle}>
             <TourStopContainer info={GraphTourStops.TimeRange}>
-              <TimeDurationContainer
-                id="graph_time_range"
-                disabled={this.props.disabled}
-                supportsReplay={true}
-              />
+              <TimeDurationContainer id="graph_time_range" disabled={this.props.disabled} supportsReplay={true} />
             </TourStopContainer>
           </div>
         </div>
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphSettings.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphSettings.tsx
index 886a955f4..b8420317d 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphSettings.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphSettings.tsx
@@ -23,7 +23,7 @@ import {
   titleStyle
 } from 'styles/DropdownStyles';
 import { INITIAL_GRAPH_STATE } from 'reducers/GraphDataState';
-import { KialiDispatch } from "types/Redux";
+import { KialiDispatch } from 'types/Redux';
 import { KialiCrippledFeatures } from 'types/ServerConfig';
 import { getCrippledFeatures } from 'services/Api';
 
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphShortcuts.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphShortcuts.tsx
index 04b8898ff..f811180f0 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphShortcuts.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphShortcuts.tsx
@@ -36,4 +36,4 @@ const GraphShortcuts = (): JSX.Element => (
   </>
 );
 
-export default GraphShortcuts;
\ No newline at end of file
+export default GraphShortcuts;
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx
index 8be03e7ea..a2e8a15e6 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx
@@ -25,7 +25,7 @@ import GraphFindContainer from './GraphFind';
 import GraphSettingsContainer from './GraphSettings';
 import history, { HistoryManager, URLParam } from '../../../app/History';
 import Namespace, { namespacesFromString, namespacesToString } from '../../../types/Namespace';
-import { KialiDispatch } from "../../../types/Redux";
+import { KialiDispatch } from '../../../types/Redux';
 import { NamespaceActions } from '../../../actions/NamespaceAction';
 import { GraphActions } from '../../../actions/GraphActions';
 import { GraphTourStops } from 'pages/Graph/GraphHelpTour';
@@ -184,7 +184,7 @@ export class GraphToolbar extends React.PureComponent<GraphToolbarProps> {
           onGraphTypeChange={this.props.setGraphType}
         />
         <Toolbar style={{ width: '100%' }}>
-          <ToolbarGroup aria-label="graph settings" style={{ margin: 0, alignItems: "flex-start"}}>
+          <ToolbarGroup aria-label="graph settings" style={{ margin: 0, alignItems: 'flex-start' }}>
             {this.props.node && (
               <ToolbarItem style={{ margin: 0 }}>
                 <Tooltip key={'graph-tour-help-ot'} position={TooltipPosition.right} content={'Back to full graph'}>
diff --git a/frontend/src/pages/Graph/SummaryLink.tsx b/frontend/src/pages/Graph/SummaryLink.tsx
index 071636c34..259b12147 100644
--- a/frontend/src/pages/Graph/SummaryLink.tsx
+++ b/frontend/src/pages/Graph/SummaryLink.tsx
@@ -7,7 +7,7 @@ import { Health } from 'types/Health';
 import { HealthIndicator } from 'components/Health/HealthIndicator';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { serverConfig } from 'config';
-import KialiPageLinkContainer from "components/Link/KialiPageLink";
+import KialiPageLinkContainer from 'components/Link/KialiPageLink';
 
 interface LinkInfo {
   link: string;
@@ -139,7 +139,7 @@ export const getLink = (nodeData: GraphNodeData, nodeType?: NodeType, linkGenera
 
   if (link && !nodeData.isInaccessible) {
     return (
-      <KialiPageLinkContainer key={key} href={link} cluster={cluster} >
+      <KialiPageLinkContainer key={key} href={link} cluster={cluster}>
         {displayName}
       </KialiPageLinkContainer>
     );
diff --git a/frontend/src/pages/Graph/SummaryPanel.tsx b/frontend/src/pages/Graph/SummaryPanel.tsx
index 1bfc8dc65..9d1bcb0e2 100644
--- a/frontend/src/pages/Graph/SummaryPanel.tsx
+++ b/frontend/src/pages/Graph/SummaryPanel.tsx
@@ -16,9 +16,9 @@ import { CyNode } from 'components/CytoscapeGraph/CytoscapeGraphUtils';
 import { GraphTourStops } from 'pages/Graph/GraphHelpTour';
 import TourStopContainer from 'components/Tour/TourStop';
 import { summaryPanelWidth } from './SummaryPanelCommon';
-import { WizardAction, WizardMode } from "components/IstioWizards/WizardActions";
-import { ServiceDetailsInfo } from "../../types/ServiceInfo";
-import { PeerAuthentication } from "../../types/IstioObjects";
+import { WizardAction, WizardMode } from 'components/IstioWizards/WizardActions';
+import { ServiceDetailsInfo } from '../../types/ServiceInfo';
+import { PeerAuthentication } from '../../types/IstioObjects';
 
 type SummaryPanelState = {
   isVisible: boolean;
@@ -29,7 +29,14 @@ type MainSummaryPanelPropType = SummaryPanelPropType & {
   jaegerState: JaegerState;
   kiosk: string;
   onDeleteTrafficRouting?: (key: string, serviceDetails: ServiceDetailsInfo) => void;
-  onLaunchWizard?: (key: WizardAction, mode: WizardMode, namespace: string, serviceDetails: ServiceDetailsInfo, gateways: string[], peerAuths: PeerAuthentication[]) => void;
+  onLaunchWizard?: (
+    key: WizardAction,
+    mode: WizardMode,
+    namespace: string,
+    serviceDetails: ServiceDetailsInfo,
+    gateways: string[],
+    peerAuths: PeerAuthentication[]
+  ) => void;
 };
 
 const mainStyle = style({
diff --git a/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx b/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx
index ff388ca56..a29fb9576 100644
--- a/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx
@@ -14,7 +14,7 @@ import {
   getAccumulatedTrafficRateTcp
 } from 'utils/TrafficRate';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
-import KialiPageLinkContainer from "components/Link/KialiPageLink";
+import KialiPageLinkContainer from 'components/Link/KialiPageLink';
 
 type SummaryPanelClusterBoxState = {
   clusterBox: any;
diff --git a/frontend/src/pages/Graph/SummaryPanelNode.tsx b/frontend/src/pages/Graph/SummaryPanelNode.tsx
index 485b31ad5..515384c76 100644
--- a/frontend/src/pages/Graph/SummaryPanelNode.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelNode.tsx
@@ -28,13 +28,13 @@ import SimpleTabs from 'components/Tab/SimpleTabs';
 import { JaegerState } from 'reducers/JaegerState';
 import { classes, style } from 'typestyle';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
-import { ServiceDetailsInfo } from "types/ServiceInfo";
-import { LoadingWizardActionsDropdownGroup } from "components/IstioWizards/LoadingWizardActionsDropdownGroup";
-import { WizardAction, WizardMode } from "components/IstioWizards/WizardActions";
-import ServiceWizardActionsDropdownGroup from "components/IstioWizards/ServiceWizardActionsDropdownGroup";
-import { PeerAuthentication } from "../../types/IstioObjects";
-import { useServiceDetailForGraphNode } from "../../hooks/services";
-import { useKialiSelector } from "../../hooks/redux";
+import { ServiceDetailsInfo } from 'types/ServiceInfo';
+import { LoadingWizardActionsDropdownGroup } from 'components/IstioWizards/LoadingWizardActionsDropdownGroup';
+import { WizardAction, WizardMode } from 'components/IstioWizards/WizardActions';
+import ServiceWizardActionsDropdownGroup from 'components/IstioWizards/ServiceWizardActionsDropdownGroup';
+import { PeerAuthentication } from '../../types/IstioObjects';
+import { useServiceDetailForGraphNode } from '../../hooks/services';
+import { useKialiSelector } from '../../hooks/redux';
 
 type SummaryPanelNodeState = {
   isActionOpen: boolean;
@@ -53,15 +53,23 @@ type ReduxProps = {
 
 export type SummaryPanelNodeHocProps = Omit<SummaryPanelPropType, 'kiosk'> & {
   onDeleteTrafficRouting?: (key: string, serviceDetails: ServiceDetailsInfo) => void;
-  onLaunchWizard?: (key: WizardAction, mode: WizardMode, namespace: string, serviceDetails: ServiceDetailsInfo, gateways: string[], peerAuths: PeerAuthentication[]) => void;
+  onLaunchWizard?: (
+    key: WizardAction,
+    mode: WizardMode,
+    namespace: string,
+    serviceDetails: ServiceDetailsInfo,
+    gateways: string[],
+    peerAuths: PeerAuthentication[]
+  ) => void;
 };
 
-export type SummaryPanelNodeProps = ReduxProps & SummaryPanelNodeHocProps & {
-  gateways: string[] | null;
-  onKebabToggled?: (isOpen: boolean) => void;
-  peerAuthentications: PeerAuthentication[] | null;
-  serviceDetails: ServiceDetailsInfo | null | undefined;
-};
+export type SummaryPanelNodeProps = ReduxProps &
+  SummaryPanelNodeHocProps & {
+    gateways: string[] | null;
+    onKebabToggled?: (isOpen: boolean) => void;
+    peerAuthentications: PeerAuthentication[] | null;
+    serviceDetails: ServiceDetailsInfo | null | undefined;
+  };
 
 const expandableSectionStyle = style({
   fontSize: 'var(--graph-side-panel--font-size)',
@@ -135,9 +143,7 @@ export class SummaryPanelNode extends React.Component<SummaryPanelNodeProps, Sum
 
     if (nodeType === NodeType.SERVICE) {
       if (this.props.serviceDetails === undefined) {
-        items.push(
-          <LoadingWizardActionsDropdownGroup />
-        );
+        items.push(<LoadingWizardActionsDropdownGroup />);
       } else if (this.props.serviceDetails !== null) {
         items.push(
           <ServiceWizardActionsDropdownGroup
@@ -146,7 +152,8 @@ export class SummaryPanelNode extends React.Component<SummaryPanelNodeProps, Sum
             k8sHTTPRoutes={this.props.serviceDetails.k8sHTTPRoutes || []}
             istioPermissions={this.props.serviceDetails.istioPermissions}
             onAction={this.handleLaunchWizard}
-            onDelete={this.handleDeleteTrafficRouting} />
+            onDelete={this.handleDeleteTrafficRouting}
+          />
         );
       }
     }
@@ -319,8 +326,10 @@ export class SummaryPanelNode extends React.Component<SummaryPanelNodeProps, Sum
     const shouldRenderGatewayHostnames =
       (nodeData.isGateway?.ingressInfo?.hostnames !== undefined &&
         nodeData.isGateway.ingressInfo.hostnames.length !== 0) ||
-      (nodeData.isGateway?.egressInfo?.hostnames !== undefined && nodeData.isGateway.egressInfo.hostnames.length !== 0) ||
-      (nodeData.isGateway?.gatewayAPIInfo?.hostnames !== undefined && nodeData.isGateway.gatewayAPIInfo.hostnames.length !== 0);
+      (nodeData.isGateway?.egressInfo?.hostnames !== undefined &&
+        nodeData.isGateway.egressInfo.hostnames.length !== 0) ||
+      (nodeData.isGateway?.gatewayAPIInfo?.hostnames !== undefined &&
+        nodeData.isGateway.gatewayAPIInfo.hostnames.length !== 0);
     const shouldRenderVsHostnames = nodeData.hasVS?.hostnames !== undefined && nodeData.hasVS?.hostnames.length !== 0;
     const shouldRenderRank = this.props.showRank;
     return (
@@ -434,7 +443,14 @@ export class SummaryPanelNode extends React.Component<SummaryPanelNodeProps, Sum
     if (this.props.onLaunchWizard) {
       const node = this.props.data.summaryTarget;
       const nodeData = decoratedNodeData(node);
-      this.props.onLaunchWizard(key, mode, nodeData.namespace, this.props.serviceDetails!, this.props.gateways!, this.props.peerAuthentications!);
+      this.props.onLaunchWizard(
+        key,
+        mode,
+        nodeData.namespace,
+        this.props.serviceDetails!,
+        this.props.gateways!,
+        this.props.peerAuthentications!
+      );
     }
   };
 
@@ -443,7 +459,7 @@ export class SummaryPanelNode extends React.Component<SummaryPanelNodeProps, Sum
     if (this.props.onDeleteTrafficRouting) {
       this.props.onDeleteTrafficRouting(key, this.props.serviceDetails!);
     }
-  }
+  };
 }
 
 export default function SummaryPanelNodeHOC(props: SummaryPanelNodeHocProps) {
@@ -457,7 +473,12 @@ export default function SummaryPanelNodeHOC(props: SummaryPanelNodeHocProps) {
 
   const node = props.data.summaryTarget;
   const nodeData = decoratedNodeData(node);
-  const [serviceDetails, gateways, peerAuthentications, isServiceDetailsLoading] = useServiceDetailForGraphNode(nodeData, isKebabOpen, props.duration, updateTime);
+  const [serviceDetails, gateways, peerAuthentications, isServiceDetailsLoading] = useServiceDetailForGraphNode(
+    nodeData,
+    isKebabOpen,
+    props.duration,
+    updateTime
+  );
 
   function handleKebabToggled(isOpen: boolean) {
     setIsKebabOpen(isOpen);
@@ -473,7 +494,7 @@ export default function SummaryPanelNodeHOC(props: SummaryPanelNodeHocProps) {
       gateways={gateways}
       peerAuthentications={peerAuthentications}
       onKebabToggled={handleKebabToggled}
-      {...props} />
+      {...props}
+    />
   );
 }
-
diff --git a/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx b/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
index dc95d7a93..a6e252e98 100644
--- a/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelNodeTraces.tsx
@@ -17,8 +17,8 @@ import { TraceListItem } from 'components/JaegerIntegration/TraceListItem';
 import { summaryFont } from './SummaryPanelCommon';
 import { DecoratedGraphNodeData } from 'types/Graph';
 import transformTraceData from 'utils/tracing/TraceTransform';
-import {isParentKiosk, kioskContextMenuAction} from "../../components/Kiosk/KioskActions";
-import { KialiDispatch } from "../../types/Redux";
+import { isParentKiosk, kioskContextMenuAction } from '../../components/Kiosk/KioskActions';
+import { KialiDispatch } from '../../types/Redux';
 
 type ReduxProps = {
   kiosk: string;
diff --git a/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx b/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx
index fd638d7be..399411b99 100644
--- a/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx
@@ -25,10 +25,10 @@ import { FormattedTraceInfo, shortIDStyle } from 'components/JaegerIntegration/J
 import SimplerSelect from 'components/SimplerSelect';
 import { summaryFont, summaryTitle } from './SummaryPanelCommon';
 import { NodeParamsType, GraphType } from 'types/Graph';
-import { KialiDispatch } from "types/Redux";
+import { KialiDispatch } from 'types/Redux';
 import { bindActionCreators } from 'redux';
 import responseFlags from 'utils/ResponseFlags';
-import {isParentKiosk, kioskContextMenuAction} from "../../components/Kiosk/KioskActions";
+import { isParentKiosk, kioskContextMenuAction } from '../../components/Kiosk/KioskActions';
 
 type ReduxProps = {
   close: () => void;
@@ -143,7 +143,8 @@ class SummaryPanelTraceDetails extends React.Component<Props, State> {
                     kioskContextMenuAction(tracesDetailsURL);
                   }
                 }}
-              >{title}
+              >
+                {title}
               </Link>
             </Tooltip>
           ) : (
@@ -243,7 +244,9 @@ class SummaryPanelTraceDetails extends React.Component<Props, State> {
                   kioskContextMenuAction(spanURL);
                 }
               }}
-            >Show span</Link>
+            >
+              Show span
+            </Link>
           </div>
         )}
       </>
@@ -358,7 +361,7 @@ class SummaryPanelTraceDetails extends React.Component<Props, State> {
 }
 
 const mapStateToProps = (state: KialiAppState) => ({
-  kiosk: state.globalState.kiosk,
+  kiosk: state.globalState.kiosk
 });
 
 const mapDispatchToProps = (dispatch: KialiDispatch) => ({
diff --git a/frontend/src/pages/Graph/__tests__/SummaryLink.test.tsx b/frontend/src/pages/Graph/__tests__/SummaryLink.test.tsx
index f662c26f0..90d6a1a61 100644
--- a/frontend/src/pages/Graph/__tests__/SummaryLink.test.tsx
+++ b/frontend/src/pages/Graph/__tests__/SummaryLink.test.tsx
@@ -47,9 +47,7 @@ describe('renderBadgedLink', () => {
 
     const wrapper = mount(
       <Provider store={store}>
-        <MemoryRouter>
-          {renderBadgedLink(node, undefined, undefined, () => linkInfo)}
-        </MemoryRouter>
+        <MemoryRouter>{renderBadgedLink(node, undefined, undefined, () => linkInfo)}</MemoryRouter>
       </Provider>
     );
     const linkNode = wrapper.find('a').filter(`[href="${linkInfo.link}"]`);
diff --git a/frontend/src/pages/IstioConfigDetails/IstioConfigDetailsPage.tsx b/frontend/src/pages/IstioConfigDetails/IstioConfigDetailsPage.tsx
index b977e58e0..212e278d1 100644
--- a/frontend/src/pages/IstioConfigDetails/IstioConfigDetailsPage.tsx
+++ b/frontend/src/pages/IstioConfigDetails/IstioConfigDetailsPage.tsx
@@ -1,11 +1,6 @@
 import * as React from 'react';
 import { Prompt, RouteComponentProps } from 'react-router-dom';
-import {
-  aceOptions,
-  IstioConfigDetails,
-  IstioConfigId,
-  safeDumpOptions
-} from '../../types/IstioConfigDetails';
+import { aceOptions, IstioConfigDetails, IstioConfigId, safeDumpOptions } from '../../types/IstioConfigDetails';
 import * as AlertUtils from '../../utils/AlertUtils';
 import * as API from '../../services/Api';
 import AceEditor from 'react-ace';
@@ -50,16 +45,16 @@ import {
 import { dicIstioType } from '../../types/IstioConfigList';
 import { showInMessageCenter } from '../../utils/IstioValidationUtils';
 import { AxiosError } from 'axios';
-import RefreshContainer from "../../components/Refresh/Refresh";
+import RefreshContainer from '../../components/Refresh/Refresh';
 import IstioConfigOverview from './IstioObjectDetails/IstioConfigOverview';
 import { Annotation } from 'react-ace/types';
-import RenderHeaderContainer from "../../components/Nav/Page/RenderHeader";
-import {ErrorMsg} from "../../types/ErrorMsg";
-import ErrorSection from "../../components/ErrorSection/ErrorSection";
-import RefreshNotifier from "../../components/Refresh/RefreshNotifier";
-import {isParentKiosk} from "../../components/Kiosk/KioskActions";
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
+import RenderHeaderContainer from '../../components/Nav/Page/RenderHeader';
+import { ErrorMsg } from '../../types/ErrorMsg';
+import ErrorSection from '../../components/ErrorSection/ErrorSection';
+import RefreshNotifier from '../../components/Refresh/RefreshNotifier';
+import { isParentKiosk } from '../../components/Kiosk/KioskActions';
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
 
 // Enables the search box for the ACEeditor
 require('ace-builds/src-noconflict/ext-searchbox');
@@ -94,6 +89,7 @@ const paramToTab: { [key: string]: number } = {
 
 interface IstioConfigDetailsProps extends RouteComponentProps<IstioConfigId> {
   kiosk: string;
+  istioAPIEnabled: boolean;
 }
 
 class IstioConfigDetailsPageComponent extends React.Component<IstioConfigDetailsProps, IstioConfigDetailsState> {
@@ -142,7 +138,8 @@ class IstioConfigDetailsPageComponent extends React.Component<IstioConfigDetails
   };
 
   fetchIstioObjectDetailsFromProps = (props: IstioConfigId) => {
-    const promiseConfigDetails = this.newIstioObjectPromise(props, true);
+    const validate = this.props.istioAPIEnabled ? true : false;
+    const promiseConfigDetails = this.newIstioObjectPromise(props, validate);
 
     // Note that adapters/templates are not supported yet for validations
     promiseConfigDetails
@@ -162,7 +159,10 @@ class IstioConfigDetailsPageComponent extends React.Component<IstioConfigDetails
         );
       })
       .catch(error => {
-        const msg : ErrorMsg = {title: 'No Istio object is selected', description: this.props.match.params.object +" is not found in the mesh"};
+        const msg: ErrorMsg = {
+          title: 'No Istio object is selected',
+          description: this.props.match.params.object + ' is not found in the mesh'
+        };
         this.setState({
           isRemoved: true,
           error: msg
@@ -482,6 +482,8 @@ class IstioConfigDetailsPageComponent extends React.Component<IstioConfigDetails
                     workloadReferences={workloadReferences}
                     helpMessages={helpMessages}
                     selectedLine={this.state.selectedEditorLine}
+                    kiosk={this.props.kiosk}
+                    istioAPIEnabled={this.props.istioAPIEnabled}
                   />
                 )}
               </>
@@ -548,7 +550,9 @@ class IstioConfigDetailsPageComponent extends React.Component<IstioConfigDetails
         overview={this.state.isExpanded}
         onOverview={this.onDrawerToggle}
       />
-    ) : ('');
+    ) : (
+      ''
+    );
   };
 
   renderActions = () => {
@@ -579,27 +583,25 @@ class IstioConfigDetailsPageComponent extends React.Component<IstioConfigDetails
           rightToolbar={<RefreshContainer id="config_details_refresh" hideLabel={true} />}
           actionsToolbar={!this.state.error ? this.renderActions() : undefined}
         />
-        {this.state.error && (
-          <ErrorSection error={this.state.error} />
-        )}
+        {this.state.error && <ErrorSection error={this.state.error} />}
         {!this.state.error && !isParentKiosk(this.props.kiosk) && (
-        <ParameterizedTabs
-          id="basic-tabs"
-          onSelect={tabValue => {
-            this.setState({ currentTab: tabValue });
-          }}
-          tabMap={paramToTab}
-          tabName={tabName}
-          defaultTab={this.defaultTab()}
-          activeTab={this.state.currentTab}
-          mountOnEnter={false}
-          unmountOnExit={true}
-        >
-          <Tab key="istio-yaml" title={`YAML ${this.state.isModified ? ' * ' : ''}`} eventKey={0}>
-            <RenderComponentScroll>{this.renderEditor()}</RenderComponentScroll>
-          </Tab>
-        </ParameterizedTabs>
-          )}
+          <ParameterizedTabs
+            id="basic-tabs"
+            onSelect={tabValue => {
+              this.setState({ currentTab: tabValue });
+            }}
+            tabMap={paramToTab}
+            tabName={tabName}
+            defaultTab={this.defaultTab()}
+            activeTab={this.state.currentTab}
+            mountOnEnter={false}
+            unmountOnExit={true}
+          >
+            <Tab key="istio-yaml" title={`YAML ${this.state.isModified ? ' * ' : ''}`} eventKey={0}>
+              <RenderComponentScroll>{this.renderEditor()}</RenderComponentScroll>
+            </Tab>
+          </ParameterizedTabs>
+        )}
         {!this.state.error && isParentKiosk(this.props.kiosk) && (
           <RenderComponentScroll>{this.renderEditor()}</RenderComponentScroll>
         )}
@@ -623,6 +625,7 @@ class IstioConfigDetailsPageComponent extends React.Component<IstioConfigDetails
 
 const mapStateToProps = (state: KialiAppState) => ({
   kiosk: state.globalState.kiosk,
+  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled
 });
 
 const IstioConfigDetailsPage = connect(mapStateToProps, null)(IstioConfigDetailsPageComponent);
diff --git a/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioConfigOverview.tsx b/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioConfigOverview.tsx
index 0a3e8e363..01e4a9353 100644
--- a/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioConfigOverview.tsx
+++ b/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioConfigOverview.tsx
@@ -1,12 +1,4 @@
-import {
-  Label,
-  Stack,
-  StackItem,
-  Title,
-  TitleSizes,
-  Tooltip,
-  TooltipPosition
-} from '@patternfly/react-core';
+import { Label, Stack, StackItem, Title, TitleSizes, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import Labels from 'components/Label/Labels';
 import { PFBadge } from 'components/Pf/PfBadges';
 import LocalTime from 'components/Time/LocalTime';
@@ -30,9 +22,9 @@ import IstioConfigHelp from './IstioConfigHelp';
 import IstioConfigReferences from './IstioConfigReferences';
 import IstioConfigValidationReferences from './IstioConfigValidationReferences';
 import IstioStatusMessageList from './IstioStatusMessageList';
-import { KioskElement } from "../../../components/Kiosk/KioskElement";
-import {PFColors} from "../../../components/Pf/PfColors";
-import {GetIstioObjectUrl} from "../../../components/Link/IstioObjectLink";
+import { KioskElement } from '../../../components/Kiosk/KioskElement';
+import { PFColors } from '../../../components/Pf/PfColors';
+import { GetIstioObjectUrl } from '../../../components/Link/IstioObjectLink';
 
 interface IstioConfigOverviewProps {
   istioObjectDetails: IstioConfigDetails;
@@ -44,6 +36,8 @@ interface IstioConfigOverviewProps {
   workloadReferences: WorkloadReference[];
   helpMessages?: HelpMessage[];
   selectedLine?: string;
+  kiosk: string;
+  istioAPIEnabled: boolean;
 }
 
 const iconStyle = style({
@@ -58,6 +52,11 @@ const infoStyle = style({
   verticalAlign: '-5px !important'
 });
 
+const warnStyle = style({
+  margin: '0px 0px 2px 0px',
+  verticalAlign: '-3px !important'
+});
+
 const healthIconStyle = style({
   marginLeft: '10px',
   verticalAlign: '-1px !important'
@@ -105,10 +104,12 @@ class IstioConfigOverview extends React.Component<IstioConfigOverviewProps> {
       </div>
     );
 
-    let urlInKiali = "";
+    let urlInKiali = '';
     if (istioObject !== undefined && istioObject.metadata.namespace !== undefined && istioObject.kind !== undefined) {
       // here a "/console" is required as external link is used
-      urlInKiali = "/console" + GetIstioObjectUrl(istioObject.metadata.name, istioObject.metadata.namespace , istioObject.kind.toLowerCase() );
+      urlInKiali =
+        '/console' +
+        GetIstioObjectUrl(istioObject.metadata.name, istioObject.metadata.namespace, istioObject.kind.toLowerCase());
     }
 
     return (
@@ -131,17 +132,17 @@ class IstioConfigOverview extends React.Component<IstioConfigOverviewProps> {
               >
                 <KialiIcon.Info className={infoStyle} />
               </Tooltip>
-              {this.props.istioValidations && (!this.props.statusMessages || this.props.statusMessages.length === 0)
-                && (!this.props.istioValidations.checks || this.props.istioValidations.checks.length === 0)
-                && (
-                <span className={healthIconStyle}>
-                  <ValidationObjectSummary
-                    id={'config-validation'}
-                    validations={[this.props.istioValidations]}
-                    reconciledCondition={getReconciliationCondition(this.props.istioObjectDetails)}
-                  />
-                </span>
-              )}
+              {this.props.istioValidations &&
+                (!this.props.statusMessages || this.props.statusMessages.length === 0) &&
+                (!this.props.istioValidations.checks || this.props.istioValidations.checks.length === 0) && (
+                  <span className={healthIconStyle}>
+                    <ValidationObjectSummary
+                      id={'config-validation'}
+                      validations={[this.props.istioValidations]}
+                      reconciledCondition={getReconciliationCondition(this.props.istioObjectDetails)}
+                    />
+                  </span>
+                )}
             </>
           )}
         </StackItem>
@@ -152,8 +153,10 @@ class IstioConfigOverview extends React.Component<IstioConfigOverviewProps> {
           </StackItem>
         )}
 
-        {((this.props.statusMessages && this.props.statusMessages.length > 0 ) ||
-          (this.props.istioValidations && this.props.istioValidations.checks && this.props.istioValidations.checks.length > 0 ) ) && (
+        {((this.props.statusMessages && this.props.statusMessages.length > 0) ||
+          (this.props.istioValidations &&
+            this.props.istioValidations.checks &&
+            this.props.istioValidations.checks.length > 0)) && (
           <StackItem>
             <IstioStatusMessageList messages={this.props.statusMessages} checks={this.props.istioValidations?.checks} />
           </StackItem>
@@ -184,12 +187,32 @@ class IstioConfigOverview extends React.Component<IstioConfigOverviewProps> {
             ></IstioConfigHelp>
           </StackItem>
         )}
+        {!this.props.istioAPIEnabled && (
+          <StackItem>
+            <KialiIcon.Warning className={warnStyle} /> <b>Istio API is disabled.</b> Be careful when editing the
+            configuration as the Istio config validations are disabled when the Istio API is disabled.
+          </StackItem>
+        )}
         <KioskElement>
           <StackItem>
-            <Tooltip content={"This is a Read only view of the YAML including Validations. It is possible to edit directly in Kiali "} position={TooltipPosition.top}>
-              <Label color="green" isCompact>Read only mode</Label>
+            <Tooltip
+              content={
+                'This is a Read only view of the YAML including Validations. It is possible to edit directly in Kiali '
+              }
+              position={TooltipPosition.top}
+            >
+              <Label color="green" isCompact>
+                Read only mode
+              </Label>
             </Tooltip>
-            <a href={urlInKiali} style={{marginLeft: '5px', fontSize: '85%', color: PFColors.ActiveText}} target="_blank" rel="noreferrer">Edit in Kiali</a>
+            <a
+              href={urlInKiali}
+              style={{ marginLeft: '5px', fontSize: '85%', color: PFColors.ActiveText }}
+              target="_blank"
+              rel="noreferrer"
+            >
+              Edit in Kiali
+            </a>
           </StackItem>
         </KioskElement>
       </Stack>
diff --git a/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioStatusMessageList.tsx b/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioStatusMessageList.tsx
index c8c704b33..d6ebab120 100644
--- a/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioStatusMessageList.tsx
+++ b/frontend/src/pages/IstioConfigDetails/IstioObjectDetails/IstioStatusMessageList.tsx
@@ -1,9 +1,9 @@
 import * as React from 'react';
-import {IstioLevelToSeverity, ObjectCheck, ValidationMessage, ValidationTypes} from '../../../types/IstioObjects';
-import {Flex, FlexItem, Stack, StackItem, Title, TitleSizes, Tooltip, TooltipPosition} from '@patternfly/react-core';
+import { IstioLevelToSeverity, ObjectCheck, ValidationMessage, ValidationTypes } from '../../../types/IstioObjects';
+import { Flex, FlexItem, Stack, StackItem, Title, TitleSizes, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import Validation from '../../../components/Validations/Validation';
-import {KialiIcon} from "../../../config/KialiIcon";
-import {style} from "typestyle";
+import { KialiIcon } from '../../../config/KialiIcon';
+import { style } from 'typestyle';
 
 interface Props {
   messages?: ValidationMessage[];
@@ -51,16 +51,13 @@ class IstioStatusMessageList extends React.Component<Props> {
                     <FlexItem>
                       <Validation severity={severity} />
                     </FlexItem>
-                    <FlexItem>
-                      {check.code}
-                    </FlexItem>
+                    <FlexItem>{check.code}</FlexItem>
                     <Tooltip content={check.message} position={TooltipPosition.right}>
                       <div className="iconInfo">
-                      <KialiIcon.Info className={infoStyle} />
+                        <KialiIcon.Info className={infoStyle} />
                       </div>
                     </Tooltip>
                   </Flex>
-
                 </StackItem>
               );
             })}
diff --git a/frontend/src/pages/IstioConfigDetails/ValidationReferences.tsx b/frontend/src/pages/IstioConfigDetails/ValidationReferences.tsx
index 14a47c941..8d69e032a 100644
--- a/frontend/src/pages/IstioConfigDetails/ValidationReferences.tsx
+++ b/frontend/src/pages/IstioConfigDetails/ValidationReferences.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { Stack, StackItem, Title, TitleSizes} from '@patternfly/react-core';
+import { Stack, StackItem, Title, TitleSizes } from '@patternfly/react-core';
 import { ReferenceIstioObjectLink } from '../../components/Link/IstioObjectLink';
 import { ObjectReference } from '../../types/IstioObjects';
 
diff --git a/frontend/src/pages/IstioConfigList/IstioConfigListPage.tsx b/frontend/src/pages/IstioConfigList/IstioConfigListPage.tsx
index 40cbe4ce3..f5aa612da 100644
--- a/frontend/src/pages/IstioConfigList/IstioConfigListPage.tsx
+++ b/frontend/src/pages/IstioConfigList/IstioConfigListPage.tsx
@@ -31,6 +31,7 @@ import DefaultSecondaryMasthead from '../../components/DefaultSecondaryMasthead/
 interface IstioConfigListPageState extends FilterComponent.State<IstioConfigItem> {}
 interface IstioConfigListPageProps extends FilterComponent.Props<IstioConfigItem> {
   activeNamespaces: Namespace[];
+  istioAPIEnabled: boolean;
 }
 
 class IstioConfigListPageComponent extends FilterComponent.Component<
@@ -139,15 +140,18 @@ class IstioConfigListPageComponent extends FilterComponent.Component<
 
   // Fetch the Istio configs, apply filters and map them into flattened list items
   fetchIstioConfigs(namespaces: string[], typeFilters: string[], istioNameFilters: string[]) {
+    const validate = this.props.istioAPIEnabled ? true : false;
     // Request all configs from all namespaces, as in backend all configs are always loaded from registry
-    return this.promises.register('configs', API.getAllIstioConfigs([], typeFilters, true, '', '')).then(response => {
-      let istioItems: IstioConfigItem[] = [];
-      // filter by selected namespaces
-      namespaces.forEach(ns => {
-        istioItems = istioItems.concat(toIstioItems(filterByName(response.data[ns], istioNameFilters)));
+    return this.promises
+      .register('configs', API.getAllIstioConfigs([], typeFilters, validate, '', ''))
+      .then(response => {
+        let istioItems: IstioConfigItem[] = [];
+        // filter by selected namespaces
+        namespaces.forEach(ns => {
+          istioItems = istioItems.concat(toIstioItems(filterByName(response.data[ns], istioNameFilters)));
+        });
+        return istioItems;
       });
-      return istioItems;
-    });
   }
 
   render() {
@@ -173,7 +177,8 @@ class IstioConfigListPageComponent extends FilterComponent.Component<
 }
 
 const mapStateToProps = (state: KialiAppState) => ({
-  activeNamespaces: activeNamespacesSelector(state)
+  activeNamespaces: activeNamespacesSelector(state),
+  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled
 });
 
 const IstioConfigListPage = connect(mapStateToProps, null)(IstioConfigListPageComponent);
diff --git a/frontend/src/pages/IstioConfigList/__tests__/IstioConfigListComponent.test.ts b/frontend/src/pages/IstioConfigList/__tests__/IstioConfigListComponent.test.ts
index eb9568808..7805e358e 100644
--- a/frontend/src/pages/IstioConfigList/__tests__/IstioConfigListComponent.test.ts
+++ b/frontend/src/pages/IstioConfigList/__tests__/IstioConfigListComponent.test.ts
@@ -23,7 +23,7 @@ const mockIstioConfigList = (names: string[]): IstioConfigList => {
     validations: {},
     permissions: {},
     wasmPlugins: [],
-    telemetries: [],
+    telemetries: []
   };
   names.forEach(name => {
     testData.authorizationPolicies.push({ metadata: { name: name + '0' }, spec: {} });
diff --git a/frontend/src/pages/IstioConfigNew/GatewayForm.tsx b/frontend/src/pages/IstioConfigNew/GatewayForm.tsx
index 768171d86..62c046941 100644
--- a/frontend/src/pages/IstioConfigNew/GatewayForm.tsx
+++ b/frontend/src/pages/IstioConfigNew/GatewayForm.tsx
@@ -1,11 +1,10 @@
 import * as React from 'react';
 // Use TextInputBase like workaround while PF4 team work in https://github.com/patternfly/patternfly-react/issues/4072
 import { FormGroup, Switch, TextInputBase as TextInput } from '@patternfly/react-core';
-import { isGatewayHostValid } from '../../utils/IstioConfigUtils';
-import ServerBuilder from './GatewayForm/ServerBuilder';
 import ServerList from './GatewayForm/ServerList';
-import { Server } from '../../types/IstioObjects';
+import { Server, ServerForm, ServerTLSSettings } from '../../types/IstioObjects';
 import { isValid } from 'utils/Common';
+import { areValidHosts } from './GatewayForm/ServerBuilder';
 
 export const GATEWAY = 'Gateway';
 export const GATEWAYS = 'gateways';
@@ -21,8 +20,7 @@ export type GatewayState = {
   workloadSelectorValid: boolean;
   workloadSelectorLabels: string;
   gatewayServers: Server[];
-  addGatewayServer: Server;
-  validHosts: boolean;
+  serversForm: ServerForm[];
 };
 
 export const initGateway = (): GatewayState => ({
@@ -30,19 +28,43 @@ export const initGateway = (): GatewayState => ({
   workloadSelectorLabels: 'istio=ingressgateway',
   workloadSelectorValid: true,
   gatewayServers: [],
-  addGatewayServer: {
-    hosts: [],
-    port: {
-      number: 80,
-      name: 'http',
-      protocol: 'HTTP'
-    }
-  },
-  validHosts: false
+  serversForm: []
 });
 
 export const isGatewayStateValid = (g: GatewayState): boolean => {
-  return g.workloadSelectorValid && g.gatewayServers.length > 0;
+  return g.workloadSelectorValid && g.gatewayServers.length > 0 && areValidGateways(g.gatewayServers);
+};
+
+const areValidGateways = (servers: Server[]): boolean => {
+  return servers.every(s => {
+    return (
+      areValidHosts(s.hosts) &&
+      s.port.name !== '' &&
+      s.port.number >= 0 &&
+      s.port.number <= 65635 &&
+      isValidTLS(s.port.protocol, s.tls)
+    );
+  });
+};
+
+const isValidTLS = (protocol: string, tls: ServerTLSSettings | undefined): boolean => {
+  if (tls !== undefined) {
+    const tlsRequired = protocol === 'HTTPS' || protocol === 'TLS';
+
+    const certsValid = tlsRequired
+      ? tls.mode === 'SIMPLE' || tls.mode === 'MUTUAL'
+        ? tls.serverCertificate !== undefined &&
+          tls.serverCertificate?.length > 0 &&
+          tls.privateKey !== undefined &&
+          tls.privateKey?.length > 0
+        : true
+      : true;
+    const caValid =
+      tlsRequired && tls.mode === 'MUTUAL' ? tls.caCertificates !== undefined && tls.caCertificates?.length > 0 : true;
+
+    return certsValid && caValid;
+  }
+  return true;
 };
 
 class GatewayForm extends React.Component<Props, GatewayState> {
@@ -95,50 +117,8 @@ class GatewayForm extends React.Component<Props, GatewayState> {
     );
   };
 
-  areValidHosts = (hosts: string[]): boolean => {
-    if (hosts.length === 0) {
-      return false;
-    }
-    let isValid = true;
-    for (let i = 0; i < hosts.length; i++) {
-      if (!isGatewayHostValid(hosts[i])) {
-        isValid = false;
-        break;
-      }
-    }
-    return isValid;
-  };
-
-  onAddServer = () => {
-    this.setState(
-      prevState => {
-        prevState.gatewayServers.push(prevState.addGatewayServer);
-        return {
-          gatewayServers: prevState.gatewayServers,
-          addGatewayServer: {
-            hosts: [],
-            port: {
-              number: 80,
-              name: 'http',
-              protocol: 'HTTP'
-            }
-          }
-        };
-      },
-      () => this.props.onChange(this.state)
-    );
-  };
-
-  onRemoveServer = (index: number) => {
-    this.setState(
-      prevState => {
-        prevState.gatewayServers.splice(index, 1);
-        return {
-          gatewayServers: prevState.gatewayServers
-        };
-      },
-      () => this.props.onChange(this.state)
-    );
+  onChangeServer = (servers: Server[], serversForm: ServerForm[]) => {
+    this.setState({ gatewayServers: servers, serversForm: serversForm }, () => this.props.onChange(this.state));
   };
 
   render() {
@@ -178,18 +158,12 @@ class GatewayForm extends React.Component<Props, GatewayState> {
             />
           </FormGroup>
         )}
-        <ServerBuilder
-          onAddServer={server => {
-            this.setState(
-              {
-                addGatewayServer: server
-              },
-              () => this.onAddServer()
-            );
-          }}
-        />
-        <FormGroup label="Server List" fieldId="gwServerList">
-          <ServerList serverList={this.state.gatewayServers} onRemoveServer={this.onRemoveServer} />
+        <FormGroup label="Server List" fieldId="gwServerList" isRequired={true}>
+          <ServerList
+            serverList={this.state.gatewayServers}
+            serverForm={this.state.serversForm}
+            onChange={this.onChangeServer}
+          />
         </FormGroup>
       </>
     );
diff --git a/frontend/src/pages/IstioConfigNew/GatewayForm/AddressBuilder.tsx b/frontend/src/pages/IstioConfigNew/GatewayForm/AddressBuilder.tsx
index 35130a00d..c37fb9016 100644
--- a/frontend/src/pages/IstioConfigNew/GatewayForm/AddressBuilder.tsx
+++ b/frontend/src/pages/IstioConfigNew/GatewayForm/AddressBuilder.tsx
@@ -1,16 +1,17 @@
 import * as React from 'react';
 import { Button, ButtonVariant, FormSelect, FormSelectOption } from '@patternfly/react-core';
 import { TextInputBase as TextInput } from '@patternfly/react-core/dist/js/components/TextInput/TextInput';
-import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table';
-import { style } from 'typestyle';
-import { PFColors } from '../../../components/Pf/PfColors';
-import { PlusCircleIcon } from '@patternfly/react-icons';
-import {isGatewayHostValid, isValidIp} from '../../../utils/IstioConfigUtils';
+import { Td, Tr } from '@patternfly/react-table';
+import { TrashIcon } from '@patternfly/react-icons';
 import { Address } from '../../../types/IstioObjects';
 import { isValid } from 'utils/Common';
+import { isGatewayHostValid, isValidIp } from '../../../utils/IstioConfigUtils';
 
 type Props = {
-  onAddAddress: (address: Address) => void;
+  address: Address;
+  onRemoveAddress: (i: number) => void;
+  onChange: (address: Address, i: number) => void;
+  index: number;
 };
 
 type State = {
@@ -19,32 +20,17 @@ type State = {
   newValue: string;
 };
 
-const warningStyle = style({
-  marginLeft: 25,
-  color: PFColors.Red100,
-  textAlign: 'center'
-});
-
-const addAddressStyle = style({
-  marginLeft: 0,
-  paddingLeft: 0
-});
-
-const addressHeader: ICell[] = [
-  {
-    title: 'Type',
-    transforms: [cellWidth(20) as any],
-    props: {}
-  },
-  {
-    title: 'Value',
-    transforms: [cellWidth(20) as any],
-    props: {}
-  },
-];
-
-const addressTypes = ['IPAddress', 'Hostname'];
+export const isValidAddress = (address: Address) => {
+  if (address.type === addressTypes[0]) {
+    return isValidIp(address.value);
+  }
+  if (address.type === addressTypes[1]) {
+    return isGatewayHostValid(address.value);
+  }
+  return false;
+};
 
+export const addressTypes = ['IPAddress', 'Hostname'];
 
 class AddressBuilder extends React.Component<Props, State> {
   constructor(props: Props) {
@@ -52,103 +38,55 @@ class AddressBuilder extends React.Component<Props, State> {
     this.state = {
       isValueValid: false,
       newType: addressTypes[0],
-      newValue: '',
+      newValue: ''
     };
   }
 
-  canAddAddress = (): boolean => {
-    return this.state.isValueValid;
-  };
-
-  isValueValid = (value: string): boolean => {
-    if (this.state.newType === addressTypes[0]) {
-      return isValidIp(value)
-    }
-    if (this.state.newType === addressTypes[1]) {
-      return isGatewayHostValid(value)
-    }
-    return false;
-  };
+  onAddValue = (value: string) => {
+    const l = this.props.address;
+    l.value = value.trim();
 
-  onAddValue = (value: string, _) => {
-    this.setState({
-      newValue: value,
-      isValueValid: this.isValueValid(value)
-    });
+    this.props.onChange(l, this.props.index);
   };
 
   onAddType = (value: string, _) => {
-    this.setState({
-      newType: value
-    });
-  };
+    const l = this.props.address;
+    l.type = value.trim();
 
-  onAddAddress = () => {
-    const newAddress: Address = {
-      type: this.state.newType,
-      value: this.state.newValue,
-    };
-    this.setState(
-      {
-        isValueValid: false,
-        newType: addressTypes[0],
-        newValue: '',
-      },
-      () => this.props.onAddAddress(newAddress)
-    );
+    this.props.onChange(l, this.props.index);
   };
 
-  addressRows() {
-    return [
-      {
-        keys: 'gatewayAddressNew',
-        cells: [
-          <>
-            <FormSelect
-              value={this.state.newType}
-              id="addType"
-              name="addType"
-              onChange={this.onAddType}
-            >
-              {addressTypes.map((option, index) => (
-                <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
-              ))}
-            </FormSelect>
-          </>,
-          <>
-            <TextInput
-              value={this.state.newValue}
-              type="text"
-              id="addValue"
-              aria-describedby="add value"
-              name="addVale"
-              onChange={this.onAddValue}
-              validated={isValid(this.state.isValueValid)}
-            />
-          </>,
-        ]
-      }
-    ];
-  }
-
   render() {
     return (
-      <>
-        <Table aria-label="Address Rows" cells={addressHeader} rows={this.addressRows()}>
-          <TableHeader />
-          <TableBody />
-        </Table>
-        <Button
-          variant={ButtonVariant.link}
-          icon={<PlusCircleIcon />}
-          onClick={this.onAddAddress}
-          isDisabled={!this.canAddAddress()}
-          className={addAddressStyle}
-        >
-          Add Address to Address List
-        </Button>
-        {!this.canAddAddress() && <span className={warningStyle}>A Address needs Type and Value sections defined</span>}
-      </>
+      <Tr>
+        <Td>
+          <FormSelect value={this.props.address.type} id="addType" name="addType" onChange={this.onAddType}>
+            {addressTypes.map((option, index) => (
+              <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
+            ))}
+          </FormSelect>
+        </Td>
+        <Td>
+          <TextInput
+            value={this.props.address.value}
+            type="text"
+            id="addValue"
+            aria-describedby="add value"
+            name="addVale"
+            onChange={this.onAddValue}
+            validated={isValid(isValidAddress(this.props.address))}
+          />
+        </Td>
+        <Td>
+          <Button
+            id="deleteBtn"
+            variant={ButtonVariant.link}
+            icon={<TrashIcon />}
+            style={{ padding: 0 }}
+            onClick={() => this.props.onRemoveAddress(this.props.index)}
+          />
+        </Td>
+      </Tr>
     );
   }
 }
diff --git a/frontend/src/pages/IstioConfigNew/GatewayForm/AddressList.tsx b/frontend/src/pages/IstioConfigNew/GatewayForm/AddressList.tsx
index d12f120d2..ac617730c 100644
--- a/frontend/src/pages/IstioConfigNew/GatewayForm/AddressList.tsx
+++ b/frontend/src/pages/IstioConfigNew/GatewayForm/AddressList.tsx
@@ -1,12 +1,15 @@
 import * as React from 'react';
 import { Address } from '../../../types/IstioObjects';
-import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table';
+import { cellWidth, TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
 import { style } from 'typestyle';
+import { Button, ButtonVariant } from '@patternfly/react-core';
+import { PlusCircleIcon } from '@patternfly/react-icons';
+import AddressBuilder from './AddressBuilder';
 import { PFColors } from '../../../components/Pf/PfColors';
 
 type Props = {
   addressList: Address[];
-  onRemoveAddress: (index: number) => void;
+  onChange: (address: Address[]) => void;
 };
 
 const noAddressStyle = style({
@@ -16,7 +19,12 @@ const noAddressStyle = style({
   width: '100%'
 });
 
-const headerCells: ICell[] = [
+const addAddressStyle = style({
+  marginLeft: 0,
+  paddingLeft: 0
+});
+
+const headerCells = [
   {
     title: '',
     transforms: [cellWidth(40) as any],
@@ -26,51 +34,67 @@ const headerCells: ICell[] = [
     title: '',
     transforms: [cellWidth(60) as any],
     props: {}
-  },
+  }
 ];
 
 class AddressList extends React.Component<Props> {
-  rows = () => {
-    return this.props.addressList.map((address, i) => {
-      return {
-        key: 'address_' + i,
-        cells: [
-          <>
-            <div>{address.type}</div>
-          </>,
-          <>
-            <div>{address.value}</div>
-          </>,
-        ]
-      };
-    });
+  onAddAddress = () => {
+    const newAddress: Address = {
+      type: 'IPAddress',
+      value: ''
+    };
+    const l = this.props.addressList;
+    l.push(newAddress);
+    this.setState({}, () => this.props.onChange(l));
   };
 
-  // @ts-ignore
-  actionResolver = (rowData, { rowIndex }) => {
-    const removeAction = {
-      title: 'Remove Address',
-      // @ts-ignore
-      onClick: (event, rowIndex, rowData, extraData) => {
-        this.props.onRemoveAddress(rowIndex);
-      }
-    };
-    return [removeAction];
+  onRemoveAddress = (index: number) => {
+    const l = this.props.addressList;
+    l.splice(index, 1);
+    this.setState({}, () => this.props.onChange(l));
+  };
+
+  onChange = (address: Address, i: number) => {
+    const l = this.props.addressList;
+    l[i] = address;
+
+    this.props.onChange(l);
   };
 
   render() {
     return (
       <>
-        <Table
-          aria-label="Address List"
-          cells={headerCells}
-          rows={this.rows()}
-          // @ts-ignore
-          actionResolver={this.actionResolver}
-        >
-          <TableHeader />
-          <TableBody />
-        </Table>
+        <TableComposable aria-label="Address List">
+          <Thead>
+            <Tr>
+              {headerCells.map(e => (
+                <Th>{e.title}</Th>
+              ))}
+            </Tr>
+          </Thead>
+          <Tbody>
+            {this.props.addressList.map((address, i) => (
+              <AddressBuilder
+                address={address}
+                onRemoveAddress={this.onRemoveAddress}
+                index={i}
+                onChange={this.onChange}
+              ></AddressBuilder>
+            ))}
+            <Tr key="addTable">
+              <Td>
+                <Button
+                  variant={ButtonVariant.link}
+                  icon={<PlusCircleIcon />}
+                  onClick={this.onAddAddress}
+                  className={addAddressStyle}
+                >
+                  Add Address to Addresses List
+                </Button>
+              </Td>
+            </Tr>
+          </Tbody>
+        </TableComposable>
         {this.props.addressList.length === 0 && <div className={noAddressStyle}>No Addresses defined</div>}
       </>
     );
diff --git a/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerBuilder.tsx b/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerBuilder.tsx
index da8dabeff..3ca3ae643 100644
--- a/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerBuilder.tsx
+++ b/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerBuilder.tsx
@@ -1,106 +1,50 @@
 import * as React from 'react';
-import { Button, ButtonVariant, FormSelect, FormSelectOption } from '@patternfly/react-core';
-import { TextInputBase as TextInput } from '@patternfly/react-core/dist/js/components/TextInput/TextInput';
-import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table';
-import { style } from 'typestyle';
-import { PFColors } from '../../../components/Pf/PfColors';
-import { PlusCircleIcon } from '@patternfly/react-icons';
 import { isGatewayHostValid } from '../../../utils/IstioConfigUtils';
-import { Listener } from '../../../types/IstioObjects';
-import { isValid } from 'utils/Common';
+import { Button, ButtonVariant, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core';
+import { isValid } from '../../../utils/Common';
+import { TrashIcon } from '@patternfly/react-icons';
+import { ListenerForm } from '../K8sGatewayForm';
+import { Td, Tr } from '@patternfly/react-table';
+import { addSelectorLabels } from './ListenerList';
 
 type Props = {
-  onAddListener: (listener: Listener) => void;
+  listener: ListenerForm;
+  onRemoveListener: (i: number) => void;
+  index: number;
+  onChange: (listenerForm: ListenerForm, i: number) => void;
 };
 
-type State = {
-  isHostValid: boolean;
-  newHostname: string;
-  newPort: string;
-  newName: string;
-  newProtocol: string;
-  newFrom: string;
-  isLabelSelectorValid: boolean;
-  newSelectorLabels: string;
-};
-
-const warningStyle = style({
-  marginLeft: 25,
-  color: PFColors.Red100,
-  textAlign: 'center'
-});
-
-const addListenerStyle = style({
-  marginLeft: 0,
-  paddingLeft: 0
-});
-
-const listenerHeader: ICell[] = [
-  {
-    title: 'Name',
-    transforms: [cellWidth(20) as any],
-    props: {}
-  },
-  {
-    title: 'Hostname',
-    transforms: [cellWidth(20) as any],
-    props: {}
-  },
-  {
-    title: 'Port',
-    transforms: [cellWidth(10) as any],
-    props: {}
-  },
-  {
-    title: 'Protocol',
-    transforms: [cellWidth(10) as any],
-    props: {}
-  },
-  {
-    title: 'From Namespaces',
-    transforms: [cellWidth(10) as any],
-    props: {}
-  },
-  {
-    title: 'Labels',
-    transforms: [cellWidth(25) as any],
-    props: {}
-  }
-];
-
 // Only HTTPRoute is supported in Istio
-const protocols = ['HTTP'];
+export const protocols = ['HTTP'];
+export const allowedRoutes = ['All', 'Selector', 'Same'];
 
-const allowedRoutes = ['All', 'Selector', 'Same'];
+export const isValidName = (name: string) => {
+  return name !== undefined && name.length > 0;
+};
 
+export const isValidHostname = (hostname: string) => {
+  return hostname !== undefined && hostname.length > 0 && isGatewayHostValid(hostname);
+};
 
-class ListenerBuilder extends React.Component<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      newHostname: '',
-      isHostValid: false,
-      newPort: '',
-      newName: '',
-      newProtocol: protocols[0],
-      newFrom: allowedRoutes[2],
-      newSelectorLabels: '',
-      isLabelSelectorValid: false,
-    };
-  }
+export const isValidPort = (port: string) => {
+  return port.length > 0 && !isNaN(Number(port)) && Number(port) >= 0 && Number(port) <= 65535;
+};
 
-  canAddListener = (): boolean => {
-    const hostValid = this.state.isHostValid;
-    const portNumberValid = this.state.newPort.length > 0 && !isNaN(Number(this.state.newPort));
-    const portNameValid = this.state.newName.length > 0;
-    return hostValid && portNumberValid && portNameValid;
-  };
+export const isValidSelector = (selector: string) => {
+  return selector.length === 0 || typeof addSelectorLabels(selector) !== 'undefined';
+};
 
+class ListenerBuilder extends React.Component<Props> {
   isValidHost = (host: string): boolean => {
     return isGatewayHostValid(host);
   };
 
   onAddHostname = (value: string, _) => {
+    const l = this.props.listener;
+    l.hostname = value.trim();
+
+    this.props.onChange(l, this.props.index);
+
     this.setState({
       newHostname: value,
       isHostValid: this.isValidHost(value)
@@ -108,206 +52,113 @@ class ListenerBuilder extends React.Component<Props, State> {
   };
 
   onAddPort = (value: string, _) => {
-    this.setState({
-      newPort: value.trim()
-    });
+    const l = this.props.listener;
+    l.port = value.trim();
+
+    this.props.onChange(l, this.props.index);
   };
 
   onAddName = (value: string, _) => {
-    this.setState({
-      newName: value.trim()
-    });
+    const l = this.props.listener;
+    l.name = value.trim();
+
+    this.props.onChange(l, this.props.index);
   };
 
   onAddProtocol = (value: string, _) => {
-    this.setState({
-      newProtocol: value
-    });
-  };
+    const l = this.props.listener;
+    l.protocol = value.trim();
 
-  onAddFrom = (value: string, _) => {
-    this.setState({
-      newFrom: value
-    });
+    this.props.onChange(l, this.props.index);
   };
 
-  onAddSelectorLabels = (value: string, _) => {
-    this.setState({
-      newSelectorLabels: value
-    });
-  };
+  onAddFrom = (value: string, _) => {
+    const l = this.props.listener;
+    l.from = value.trim();
 
-  onAddListener = () => {
-    const newListener: Listener = {
-      hostname: this.state.newHostname,
-      port: +this.state.newPort,
-      name: this.state.newName,
-      protocol: this.state.newProtocol,
-      allowedRoutes: {namespaces: {from: this.state.newFrom, selector: {matchLabels: this.getLabelsMap(this.state.newSelectorLabels)}}}
-    };
-    this.setState(
-      {
-        newHostname: '',
-        isHostValid: false,
-        newPort: '',
-        newName: '',
-        newProtocol: protocols[0],
-        newFrom: allowedRoutes[2],
-        newSelectorLabels: '',
-        isLabelSelectorValid: false,
-      },
-      () => this.props.onAddListener(newListener)
-    );
+    this.props.onChange(l, this.props.index);
   };
 
-  getLabelsMap = (value: string) => {
-    const valuesMap = {}
-    value
-      .trim()
-      .split(',')
-      .forEach(split => {
-        const labels = split.trim().split('=');
-        if (labels.length === 2) {
-            valuesMap[labels[0].trim()] = labels[1].trim();
-        }
-      });
-    return valuesMap;
-  }
+  onAddSelectorLabels = (value: string, _) => {
+    const l = this.props.listener;
+    l.sSelectorLabels = value.trim();
 
-  addSelectorLabels = (value: string, _) => {
-    if (value.length === 0) {
-      this.setState(
-        {
-          isLabelSelectorValid: false,
-          newSelectorLabels: ''
-        },
-      );
-      return;
-    }
-    value = value.trim();
-    const labels: string[] = value.split(',');
-    let isValid = true;
-    // Some smoke validation rules for the labels
-    for (let i = 0; i < labels.length; i++) {
-      const label = labels[i];
-      if (label.indexOf('=') < 0) {
-        isValid = false;
-        break;
-      }
-      const splitLabel: string[] = label.split('=');
-      if (splitLabel.length !== 2) {
-        isValid = false;
-        break;
-      }
-      if (splitLabel[0].trim().length === 0 || splitLabel[1].trim().length === 0) {
-        isValid = false;
-        break;
-      }
-    }
-    this.setState(
-      {
-        isLabelSelectorValid: isValid,
-        newSelectorLabels: value
-      },
-    );
+    this.props.onChange(l, this.props.index);
   };
 
-  listenerRows() {
-    const showSelector = this.state.newFrom === 'Selector';
-    return [
-      {
-        keys: 'gatewayListenerNew',
-        cells: [
-          <>
-            <TextInput
-              value={this.state.newName}
-              type="text"
-              id="addName"
-              aria-describedby="add name"
-              name="addName"
-              onChange={this.onAddName}
-              validated={isValid(this.state.newName.length > 0)}
-            />
-          </>,
-          <>
-            <TextInput
-              value={this.state.newHostname}
-              type="text"
-              id="addHostname"
-              aria-describedby="add hostname"
-              name="addHostname"
-              onChange={this.onAddHostname}
-              validated={isValid(this.state.isHostValid)}
-            />
-          </>,
-          <>
-            <TextInput
-              value={this.state.newPort}
-              type="text"
-              id="addPort"
-              aria-describedby="add port"
-              name="addPortNumber"
-              onChange={this.onAddPort}
-              validated={isValid(this.state.newPort.length > 0 && !isNaN(Number(this.state.newPort)))}
-            />
-          </>,
-          <>
-            <FormSelect
-              value={this.state.newProtocol}
-              id="addPortProtocol"
-              name="addPortProtocol"
-              onChange={this.onAddProtocol}
-            >
-              {protocols.map((option, index) => (
-                <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
-              ))}
-            </FormSelect>
-          </>,
-          <>
-            <FormSelect
-              value={this.state.newFrom}
-              id="addFrom"
-              name="addFrom"
-              onChange={this.onAddFrom}
-            >
-              {allowedRoutes.map((option, index) => (
-                <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
-              ))}
-            </FormSelect>
-          </>,
-          <>
-            <TextInput
-              id="addSelectorLabels"
-              name="addSelectorLabels"
-              value={this.state.newSelectorLabels}
-              onChange={this.addSelectorLabels}
-              isDisabled={!showSelector}
-              validated={isValid(!showSelector || this.state.isLabelSelectorValid)}
-            />
-          </>
-        ]
-      }
-    ];
-  }
-
   render() {
     return (
-      <>
-        <Table aria-label="Listener Rows" cells={listenerHeader} rows={this.listenerRows()}>
-          <TableHeader />
-          <TableBody />
-        </Table>
-        <Button
-          variant={ButtonVariant.link}
-          icon={<PlusCircleIcon />}
-          onClick={this.onAddListener}
-          isDisabled={!this.canAddListener()}
-          className={addListenerStyle}
-        >
-          Add Listener to Listener List
-        </Button>
-        {!this.canAddListener() && <span className={warningStyle}>A Listener needs Hostname and Port sections defined</span>}
-      </>
+      <Tr>
+        <Td>
+          <TextInput
+            value={this.props.listener.name}
+            type="text"
+            id="addName"
+            aria-describedby="add name"
+            onChange={this.onAddName}
+            validated={isValid(isValidName(this.props.listener.name))}
+          />
+        </Td>
+        <Td>
+          <TextInput
+            value={this.props.listener.hostname}
+            type="text"
+            id="addHostname"
+            aria-describedby="add hostname"
+            name="addHostname"
+            onChange={this.onAddHostname}
+            validated={isValid(isValidHostname(this.props.listener.hostname))}
+          />
+        </Td>
+        <Td>
+          <TextInput
+            value={this.props.listener.port}
+            type="text"
+            id="addPort"
+            placeholder="80"
+            aria-describedby="add port"
+            name="addPortNumber"
+            onChange={this.onAddPort}
+            validated={isValid(isValidPort(this.props.listener.port))}
+          />
+        </Td>
+        <Td>
+          <FormSelect
+            value={this.props.listener.protocol}
+            id="addPortProtocol"
+            name="addPortProtocol"
+            onChange={this.onAddProtocol}
+          >
+            {protocols.map((option, index) => (
+              <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
+            ))}
+          </FormSelect>
+        </Td>
+        <Td>
+          <FormSelect value={this.props.listener.from} id="addFrom" name="addFrom" onChange={this.onAddFrom}>
+            {allowedRoutes.map((option, index) => (
+              <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
+            ))}
+          </FormSelect>
+        </Td>
+        <Td>
+          <TextInput
+            id="addSelectorLabels"
+            name="addSelectorLabels"
+            onChange={this.onAddSelectorLabels}
+            validated={isValid(isValidSelector(this.props.listener.sSelectorLabels))}
+          />
+        </Td>
+        <Td>
+          <Button
+            id="deleteBtn"
+            variant={ButtonVariant.link}
+            icon={<TrashIcon />}
+            style={{ padding: 0 }}
+            onClick={() => this.props.onRemoveListener(this.props.index)}
+          />
+        </Td>
+      </Tr>
     );
   }
 }
diff --git a/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerList.tsx b/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerList.tsx
index d1efb0eb6..8a80d54c7 100644
--- a/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerList.tsx
+++ b/frontend/src/pages/IstioConfigNew/GatewayForm/ListenerList.tsx
@@ -1,12 +1,17 @@
 import * as React from 'react';
-import { Listener } from '../../../types/IstioObjects';
-import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table';
+import { cellWidth, TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
 import { style } from 'typestyle';
 import { PFColors } from '../../../components/Pf/PfColors';
+import { Button, ButtonVariant } from '@patternfly/react-core';
+import { PlusCircleIcon } from '@patternfly/react-icons';
+import { Listener } from '../../../types/IstioObjects';
+import { ListenerForm } from '../K8sGatewayForm';
+import ListenerBuilder, { allowedRoutes, protocols } from './ListenerBuilder';
 
 type Props = {
-  listenerList: Listener[];
-  onRemoveListener: (index: number) => void;
+  onChange: (listener: Listener[], listenerForm: ListenerForm[]) => void;
+  listenersForm: ListenerForm[];
+  listeners: Listener[];
 };
 
 const noListenerStyle = style({
@@ -16,98 +21,183 @@ const noListenerStyle = style({
   width: '100%'
 });
 
-const headerCells: ICell[] = [
+const addListenerStyle = style({
+  marginLeft: 0,
+  paddingLeft: 0
+});
+
+const headerCells = [
   {
-    title: '',
+    title: 'Name',
     transforms: [cellWidth(20) as any],
     props: {}
   },
   {
-    title: '',
+    title: 'Hostname',
     transforms: [cellWidth(20) as any],
     props: {}
   },
   {
-    title: '',
+    title: 'Port',
     transforms: [cellWidth(10) as any],
     props: {}
   },
   {
-    title: '',
+    title: 'Protocol',
     transforms: [cellWidth(10) as any],
     props: {}
   },
   {
-    title: '',
+    title: 'From Namespaces',
     transforms: [cellWidth(10) as any],
     props: {}
   },
   {
-    title: '',
+    title: 'Labels',
     transforms: [cellWidth(25) as any],
     props: {}
   },
+  {
+    title: '',
+    transforms: [cellWidth(10) as any],
+    props: {}
+  }
 ];
 
-class ListenerList extends React.Component<Props> {
-  rows = () => {
-    return this.props.listenerList.map((listener, i) => {
-      return {
-        key: 'listener_' + i,
-        cells: [
-          <>
-            <div>{listener.name}</div>
-          </>,
-          <>
-            <div>{listener.hostname}</div>
-          </>,
-          <>
-            <div>
-              {listener.port}
-            </div>
-          </>,
-          <>
-            <div>
-              {listener.protocol}
-            </div>
-          </>,
-          <>
-            <div>{listener.allowedRoutes.namespaces.from}</div>
-          </>,
-          <>
-            <div>{Object.keys(listener.allowedRoutes.namespaces.selector?.matchLabels).length !== 0 ? JSON.stringify(listener.allowedRoutes.namespaces.selector.matchLabels) : ''}</div>
-          </>,
-        ]
-      };
-    });
+export const addSelectorLabels = (value: string) => {
+  if (value.length === 0) {
+    return;
+  }
+  value = value.trim();
+  const labels: string[] = value.split(',');
+
+  const selector: { [key: string]: string } = {};
+  // Some smoke validation rules for the labels
+  for (let i = 0; i < labels.length; i++) {
+    const label = labels[i];
+    if (label.indexOf('=') < 0) {
+      break;
+    }
+    const splitLabel: string[] = label.split('=');
+    if (splitLabel.length !== 2) {
+      break;
+    }
+    if (splitLabel[0].trim().length === 0 || splitLabel[1].trim().length === 0) {
+      break;
+    }
+    selector[splitLabel[0].trim()] = splitLabel[1].trim();
+  }
+  return selector;
+};
+
+type ListenerListState = {
+  keyFocus: string;
+};
+
+class ListenerList extends React.Component<Props, ListenerListState> {
+  onAddListener = () => {
+    const newListener: ListenerForm = {
+      hostname: '',
+      port: '',
+      name: '',
+      protocol: protocols[0],
+      isHostValid: false,
+      from: allowedRoutes[0],
+      isLabelSelectorValid: false,
+      sSelectorLabels: ''
+    };
+    const l = this.props.listenersForm;
+    l.push(newListener);
+
+    const newListenerF: Listener = {
+      hostname: '',
+      port: 70000,
+      name: '',
+      protocol: protocols[0],
+      allowedRoutes: { namespaces: { from: allowedRoutes[0], selector: { matchLabels: {} } } }
+    };
+
+    const lf = this.props.listeners;
+    lf.push(newListenerF);
+
+    this.setState({}, () => this.props.onChange(lf, l));
+  };
+
+  onRemoveListener = (index: number) => {
+    const l = this.props.listenersForm;
+    l.splice(index, 1);
+
+    const lf = this.props.listeners;
+    lf.splice(index, 1);
+
+    this.setState({}, () => this.props.onChange(lf, l));
   };
 
-  // @ts-ignore
-  actionResolver = (rowData, { rowIndex }) => {
-    const removeAction = {
-      title: 'Remove Listener',
-      // @ts-ignore
-      onClick: (event, rowIndex, rowData, extraData) => {
-        this.props.onRemoveListener(rowIndex);
-      }
+  onChange = (listenersForm: ListenerForm, i: number) => {
+    const lf = this.props.listenersForm;
+    lf[i] = listenersForm;
+
+    const l = this.props.listeners;
+    const newL = this.createNewListener(listenersForm);
+    if (typeof newL !== 'undefined') {
+      l[i] = newL;
+    }
+
+    this.props.onChange(l, lf);
+  };
+
+  createNewListener = (listenerForm: ListenerForm) => {
+    if (listenerForm.port.length === 0 || isNaN(Number(listenerForm.port))) return;
+    if (listenerForm.hostname.length === 0) return;
+
+    const selector = addSelectorLabels(listenerForm.sSelectorLabels) || {};
+
+    const listener: Listener = {
+      hostname: listenerForm.hostname,
+      port: Number(listenerForm.port),
+      name: listenerForm.name,
+      protocol: listenerForm.protocol,
+      allowedRoutes: { namespaces: { from: listenerForm.from, selector: { matchLabels: selector } } }
     };
-    return [removeAction];
+    return listener;
   };
 
   render() {
     return (
       <>
-        <Table
-          aria-label="Listener List"
-          cells={headerCells}
-          rows={this.rows()}
-          // @ts-ignore
-          actionResolver={this.actionResolver}
-        >
-          <TableHeader />
-          <TableBody />
-        </Table>
-        {this.props.listenerList.length === 0 && <div className={noListenerStyle}>No Listeners defined</div>}
+        <TableComposable aria-label="Listener List">
+          <Thead>
+            <Tr>
+              {headerCells.map(e => (
+                <Th>{e.title}</Th>
+              ))}
+            </Tr>
+          </Thead>
+          <Tbody>
+            {this.props.listenersForm.map((listener, i) => (
+              <ListenerBuilder
+                listener={listener}
+                onRemoveListener={this.onRemoveListener}
+                index={i}
+                onChange={this.onChange}
+              ></ListenerBuilder>
+            ))}
+            <Tr>
+              <Td>
+                <Button
+                  name="addListener"
+                  variant={ButtonVariant.link}
+                  icon={<PlusCircleIcon />}
+                  onClick={this.onAddListener}
+                  className={addListenerStyle}
+                >
+                  Add Listener to Listener List
+                </Button>
+              </Td>
+            </Tr>
+          </Tbody>
+        </TableComposable>
+        {this.props.listenersForm.length === 0 && <div className={noListenerStyle}>No Listeners defined</div>}
       </>
     );
   }
diff --git a/frontend/src/pages/IstioConfigNew/GatewayForm/ServerBuilder.tsx b/frontend/src/pages/IstioConfigNew/GatewayForm/ServerBuilder.tsx
index d61d7e988..ce5799b7e 100644
--- a/frontend/src/pages/IstioConfigNew/GatewayForm/ServerBuilder.tsx
+++ b/frontend/src/pages/IstioConfigNew/GatewayForm/ServerBuilder.tsx
@@ -1,16 +1,18 @@
 import * as React from 'react';
 import { Button, ButtonVariant, FormGroup, FormSelect, FormSelectOption } from '@patternfly/react-core';
 import { TextInputBase as TextInput } from '@patternfly/react-core/dist/js/components/TextInput/TextInput';
-import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table';
-import { style } from 'typestyle';
-import { PFColors } from '../../../components/Pf/PfColors';
-import { PlusCircleIcon } from '@patternfly/react-icons';
+import { cellWidth, TableComposable, Td, Th, Tr } from '@patternfly/react-table';
+import { TrashIcon } from '@patternfly/react-icons';
 import { isGatewayHostValid } from '../../../utils/IstioConfigUtils';
-import { Server } from '../../../types/IstioObjects';
+import { ServerForm } from '../../../types/IstioObjects';
 import { isValid } from 'utils/Common';
+import { isValidPort } from './ListenerBuilder';
 
 type Props = {
-  onAddServer: (server: Server) => void;
+  server: ServerForm;
+  onRemoveServer: (i: number) => void;
+  index: number;
+  onChange: (serverform: ServerForm, i: number) => void;
 };
 
 type State = {
@@ -25,18 +27,7 @@ type State = {
   newTlsCaCertificate: string;
 };
 
-const warningStyle = style({
-  marginLeft: 25,
-  color: PFColors.Red100,
-  textAlign: 'center'
-});
-
-const addServerStyle = style({
-  marginLeft: 0,
-  paddingLeft: 0
-});
-
-const portHeader: ICell[] = [
+const portHeader = [
   {
     title: 'Port Number',
     transforms: [cellWidth(20) as any],
@@ -54,299 +45,242 @@ const portHeader: ICell[] = [
   }
 ];
 
-const protocols = ['HTTP', 'HTTPS', 'GRPC', 'HTTP2', 'MONGO', 'TCP', 'TLS'];
-
+export const protocols = ['HTTP', 'HTTPS', 'GRPC', 'HTTP2', 'MONGO', 'TCP', 'TLS'];
 const tlsModes = ['PASSTHROUGH', 'SIMPLE', 'MUTUAL', 'AUTO_PASSTHROUGH', 'ISTIO_MUTUAL'];
 
-class ServerBuilder extends React.Component<Props, State> {
-  constructor(props: Props) {
-    super(props);
-    this.state = {
-      newHosts: [],
-      isHostsValid: false,
-      newPortNumber: '',
-      newPortName: '',
-      newPortProtocol: protocols[0],
-      newTlsMode: tlsModes[1], // SIMPLE
-      newTlsServerCertificate: '',
-      newTlsPrivateKey: '',
-      newTlsCaCertificate: ''
-    };
+export const areValidHosts = (hosts: string[]): boolean => {
+  if (hosts.length === 0) {
+    return false;
   }
-
-  canAddServer = (): boolean => {
-    const hostValid = this.state.isHostsValid;
-    const portNumberValid = this.state.newPortNumber.length > 0 && !isNaN(Number(this.state.newPortNumber));
-    const portNameValid = this.state.newPortName.length > 0;
-    const tlsRequired = this.state.newPortProtocol === 'HTTPS' || this.state.newPortProtocol === 'TLS';
-    const tlsCertsValid = tlsRequired
-      ? this.state.newTlsMode === 'SIMPLE' || this.state.newTlsMode === 'MUTUAL'
-        ? this.state.newTlsServerCertificate.length > 0 && this.state.newTlsPrivateKey.length > 0
-        : true
-      : true;
-    const tlsCaValid =
-      tlsRequired && this.state.newTlsMode === 'MUTUAL' ? this.state.newTlsCaCertificate.length > 0 : true;
-    return hostValid && portNumberValid && portNameValid && tlsCertsValid && tlsCaValid;
-  };
-
-  areValidHosts = (hosts: string[]): boolean => {
-    if (hosts.length === 0) {
-      return false;
+  let isValid = true;
+  for (let i = 0; i < hosts.length; i++) {
+    if (!isGatewayHostValid(hosts[i])) {
+      isValid = false;
+      break;
     }
-    let isValid = true;
-    for (let i = 0; i < hosts.length; i++) {
-      if (!isGatewayHostValid(hosts[i])) {
-        isValid = false;
-        break;
-      }
-    }
-    return isValid;
-  };
+  }
+  return isValid;
+};
 
+class ServerBuilder extends React.Component<Props, State> {
   onAddHosts = (value: string, _) => {
-    const hosts = value.trim().length === 0 ? [] : value.split(',').map(host => host.trim());
-    this.setState({
-      newHosts: hosts,
-      isHostsValid: this.areValidHosts(hosts)
-    });
+    const server = this.props.server;
+    server.hosts = value.trim().length === 0 ? [] : value.split(',').map(host => host.trim());
+
+    this.props.onChange(server, this.props.index);
   };
 
   onAddPortNumber = (value: string, _) => {
-    this.setState({
-      newPortNumber: value.trim()
-    });
+    const server = this.props.server;
+    server.number = value.trim();
+
+    this.props.onChange(server, this.props.index);
   };
 
   onAddPortName = (value: string, _) => {
-    this.setState({
-      newPortName: value.trim()
-    });
+    const server = this.props.server;
+    server.name = value.trim();
+
+    this.props.onChange(server, this.props.index);
   };
 
   onAddPortProtocol = (value: string, _) => {
-    this.setState({
-      newPortProtocol: value
-    });
-  };
+    const server = this.props.server;
+    server.protocol = value.trim();
 
-  onAddServer = () => {
-    const newServer: Server = {
-      hosts: this.state.newHosts,
-      port: {
-        number: +this.state.newPortNumber,
-        name: this.state.newPortName,
-        protocol: this.state.newPortProtocol
-      }
-    };
-    if (this.state.newPortProtocol === 'HTTPS' || this.state.newPortProtocol === 'TLS') {
-      newServer.tls = {
-        mode: this.state.newTlsMode
-      };
-      if (this.state.newTlsMode === 'SIMPLE' || this.state.newTlsMode === 'MUTUAL') {
-        newServer.tls.privateKey = this.state.newTlsPrivateKey;
-        newServer.tls.serverCertificate = this.state.newTlsServerCertificate;
-      }
-      if (this.state.newTlsMode === 'MUTUAL') {
-        newServer.tls.caCertificates = this.state.newTlsCaCertificate;
-      }
-    }
-    this.setState(
-      {
-        newHosts: [],
-        isHostsValid: false,
-        newPortNumber: '',
-        newPortName: '',
-        newPortProtocol: protocols[0],
-        newTlsMode: tlsModes[1], // SIMPLE
-        newTlsServerCertificate: '',
-        newTlsPrivateKey: '',
-        newTlsCaCertificate: ''
-      },
-      () => this.props.onAddServer(newServer)
-    );
+    this.props.onChange(server, this.props.index);
   };
 
   onAddTlsMode = (value: string, _) => {
-    this.setState({
-      newTlsMode: value
-    });
+    const server = this.props.server;
+    server.tlsMode = value.trim();
+
+    this.props.onChange(server, this.props.index);
   };
 
   onAddTlsServerCertificate = (value: string, _) => {
-    this.setState({
-      newTlsServerCertificate: value
-    });
+    const server = this.props.server;
+    server.tlsServerCertificate = value.trim();
+
+    this.props.onChange(server, this.props.index);
   };
 
   onAddTlsPrivateKey = (value: string, _) => {
-    this.setState({
-      newTlsPrivateKey: value
-    });
+    const server = this.props.server;
+    server.tlsPrivateKey = value.trim();
+
+    this.props.onChange(server, this.props.index);
   };
 
   onAddTlsCaCertificate = (value: string, _) => {
-    this.setState({
-      newTlsCaCertificate: value
-    });
+    const server = this.props.server;
+    server.tlsCaCertificate = value.trim();
+
+    this.props.onChange(server, this.props.index);
   };
 
   portRows() {
-    return [
-      {
-        keys: 'gatewayPortNew',
-        cells: [
-          <>
-            <TextInput
-              value={this.state.newPortNumber}
-              type="text"
-              id="addPortNumber"
-              aria-describedby="add port number"
-              name="addPortNumber"
-              onChange={this.onAddPortNumber}
-              validated={isValid(this.state.newPortNumber.length > 0 && !isNaN(Number(this.state.newPortNumber)))}
-            />
-          </>,
-          <>
-            <TextInput
-              value={this.state.newPortName}
-              type="text"
-              id="addPortName"
-              aria-describedby="add port name"
-              name="addPortName"
-              onChange={this.onAddPortName}
-              validated={isValid(this.state.newPortName.length > 0)}
-            />
-          </>,
-          <>
-            <FormSelect
-              value={this.state.newPortProtocol}
-              id="addPortProtocol"
-              name="addPortProtocol"
-              onChange={this.onAddPortProtocol}
-            >
-              {protocols.map((option, index) => (
-                <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
-              ))}
-            </FormSelect>
-          </>
-        ]
-      }
-    ];
+    return (
+      <Tr>
+        <Td style={{ padding: '0 10px 0 0' }}>
+          <TextInput
+            value={this.props.server.number}
+            type="text"
+            id="addPortNumber"
+            aria-describedby="add port number"
+            name="addPortNumber"
+            onChange={this.onAddPortNumber}
+            validated={isValid(isValidPort(this.props.server.number))}
+          />
+        </Td>
+        <Td style={{ padding: '0 10px 0 0' }}>
+          <TextInput
+            value={this.props.server.name}
+            type="text"
+            id="addPortName"
+            aria-describedby="add port name"
+            name="addPortName"
+            onChange={this.onAddPortName}
+            validated={isValid(this.props.server.name.length > 0)}
+          />
+        </Td>
+        <Td style={{ padding: '0 10px 0 0' }}>
+          <FormSelect
+            value={this.props.server.protocol}
+            id="addPortProtocol"
+            name="addPortProtocol"
+            onChange={this.onAddPortProtocol}
+          >
+            {protocols.map((option, index) => (
+              <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
+            ))}
+          </FormSelect>
+        </Td>
+      </Tr>
+    );
   }
 
   render() {
-    const showTls = this.state.newPortProtocol === 'HTTPS' || this.state.newPortProtocol === 'TLS';
+    const showTls = this.props.server.protocol === 'HTTPS' || this.props.server.protocol === 'TLS';
     return (
-      <>
-        <FormGroup
-          label="Hosts"
-          isRequired={true}
-          fieldId="gateway-selector"
-          helperText="One or more hosts exposed by this Gateway."
-          helperTextInvalid="Invalid hosts for this Gateway. Enter one or more hosts separated by comma."
-          validated={isValid(this.state.isHostsValid)}
-        >
-          <TextInput
-            value={this.state.newHosts.join(',')}
+      <Tr>
+        <Td>
+          <FormGroup
+            label="Hosts"
             isRequired={true}
-            type="text"
-            id="hosts"
-            aria-describedby="hosts"
-            name="hosts"
-            onChange={this.onAddHosts}
-            validated={isValid(this.state.isHostsValid)}
-          />
-        </FormGroup>
-        <FormGroup label="Port" isRequired={true} fieldId="server-port">
-          <Table aria-label="Port Level MTLS" cells={portHeader} rows={this.portRows()}>
-            <TableHeader />
-            <TableBody />
-          </Table>
-        </FormGroup>
-        {showTls && (
-          <FormGroup label="TLS Mode" isRequired={true} fieldId="addTlsMode">
-            <FormSelect value={this.state.newTlsMode} id="addTlsMode" name="addTlsMode" onChange={this.onAddTlsMode}>
-              {tlsModes.map((option, index) => (
-                <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
+            fieldId="gateway-selector"
+            helperText="One or more hosts exposed by this Gateway."
+            helperTextInvalid="Invalid hosts for this Gateway. Enter one or more hosts separated by comma."
+          >
+            <TextInput
+              value={this.props.server.hosts.join(',')}
+              isRequired={true}
+              type="text"
+              id="hosts"
+              aria-describedby="hosts"
+              name="hosts"
+              onChange={this.onAddHosts}
+              validated={isValid(areValidHosts(this.props.server.hosts))}
+            />
+          </FormGroup>
+          <FormGroup label="Port" isRequired={true} fieldId="server-port" style={{ padding: '10px 0' }}>
+            <TableComposable aria-label="Port Level MTLS">
+              {portHeader.map(e => (
+                <Th>{e.title}</Th>
               ))}
-            </FormSelect>
+              {this.portRows()}
+            </TableComposable>
           </FormGroup>
-        )}
-        {showTls && (this.state.newTlsMode === 'SIMPLE' || this.state.newTlsMode === 'MUTUAL') && (
-          <>
-            <FormGroup
-              label="Server Certificate"
-              isRequired={true}
-              fieldId="server-certificate"
-              validated={isValid(this.state.newTlsServerCertificate.length > 0)}
-              helperTextInvalid={'The path to the file holding the server-side TLS certificate to use.'}
-            >
-              <TextInput
-                value={this.state.newTlsServerCertificate}
-                isRequired={true}
-                type="text"
-                id="server-certificate"
-                aria-describedby="server-certificate"
-                name="server-certificate"
-                onChange={this.onAddTlsServerCertificate}
-                validated={isValid(this.state.newTlsServerCertificate.length > 0)}
-              />
+          {showTls && (
+            <FormGroup label="TLS Mode" isRequired={true} fieldId="addTlsMode" style={{ margin: '10px 0' }}>
+              <FormSelect
+                value={this.props.server.tlsMode}
+                id="addTlsMode"
+                name="addTlsMode"
+                onChange={this.onAddTlsMode}
+              >
+                {tlsModes.map((option, index) => (
+                  <FormSelectOption isDisabled={false} key={'p' + index} value={option} label={option} />
+                ))}
+              </FormSelect>
             </FormGroup>
+          )}
+          {showTls && (this.props.server.tlsMode === 'SIMPLE' || this.props.server.tlsMode === 'MUTUAL') && (
+            <>
+              <FormGroup
+                label="Server Certificate"
+                style={{ margin: '10px 0' }}
+                isRequired={true}
+                fieldId="server-certificate"
+                validated={isValid(this.props.server.tlsServerCertificate.length > 0)}
+                helperTextInvalid={'The path to the file holding the server-side TLS certificate to use.'}
+              >
+                <TextInput
+                  value={this.props.server.tlsServerCertificate}
+                  isRequired={true}
+                  type="text"
+                  id="server-certificate"
+                  aria-describedby="server-certificate"
+                  name="server-certificate"
+                  onChange={this.onAddTlsServerCertificate}
+                  validated={isValid(this.props.server.tlsServerCertificate.length > 0)}
+                />
+              </FormGroup>
+              <FormGroup
+                label="Private Key"
+                isRequired={true}
+                fieldId="private-key"
+                style={{ margin: '10px 0' }}
+                validated={isValid(this.props.server.tlsPrivateKey.length > 0)}
+                helperTextInvalid={'The path to the file holding the server’s private key.'}
+              >
+                <TextInput
+                  value={this.props.server.tlsPrivateKey}
+                  isRequired={true}
+                  type="text"
+                  id="private-key"
+                  aria-describedby="private-key"
+                  name="private-key"
+                  onChange={this.onAddTlsPrivateKey}
+                  validated={isValid(this.props.server.tlsPrivateKey.length > 0)}
+                />
+              </FormGroup>
+            </>
+          )}
+          {showTls && this.props.server.tlsMode === 'MUTUAL' && (
             <FormGroup
-              label="Private Key"
+              label="CA Certificate"
+              style={{ margin: '10px 0' }}
               isRequired={true}
-              fieldId="private-key"
-              validated={isValid(this.state.newTlsPrivateKey.length > 0)}
-              helperTextInvalid={'The path to the file holding the server’s private key.'}
+              fieldId="ca-certificate"
+              validated={isValid(this.props.server.tlsCaCertificate.length > 0)}
+              helperTextInvalid={
+                'The path to a file containing certificate authority certificates to use in verifying a presented client side certificate.'
+              }
             >
               <TextInput
-                value={this.state.newTlsPrivateKey}
+                value={this.props.server.tlsCaCertificate}
                 isRequired={true}
                 type="text"
-                id="private-key"
-                aria-describedby="private-key"
-                name="private-key"
-                onChange={this.onAddTlsPrivateKey}
-                validated={isValid(this.state.newTlsPrivateKey.length > 0)}
+                id="ca-certificate"
+                aria-describedby="ca-certificate"
+                name="ca-certificate"
+                onChange={this.onAddTlsCaCertificate}
+                validated={isValid(this.props.server.tlsCaCertificate.length > 0)}
               />
             </FormGroup>
-          </>
-        )}
-        {showTls && this.state.newTlsMode === 'MUTUAL' && (
-          <FormGroup
-            label="CA Certificate"
-            isRequired={true}
-            fieldId="ca-certificate"
-            validated={isValid(this.state.newTlsCaCertificate.length > 0)}
-            helperTextInvalid={
-              'The path to a file containing certificate authority certificates to use in verifying a presented client side certificate.'
-            }
-          >
-            <TextInput
-              value={this.state.newTlsCaCertificate}
-              isRequired={true}
-              type="text"
-              id="ca-certificate"
-              aria-describedby="ca-certificate"
-              name="ca-certificate"
-              onChange={this.onAddTlsCaCertificate}
-              validated={isValid(this.state.newTlsCaCertificate.length > 0)}
-            />
-          </FormGroup>
-        )}
-        <FormGroup fieldId="addRule">
+          )}
+        </Td>
+        <Td style={{ verticalAlign: 'middle' }}>
           <Button
+            id="deleteBtn"
             variant={ButtonVariant.link}
-            icon={<PlusCircleIcon />}
-            onClick={this.onAddServer}
-            isDisabled={!this.canAddServer()}
-            className={addServerStyle}
-          >
-            Add Server to Server List
-          </Button>
-          {!this.canAddServer() && <span className={warningStyle}>A Server needs Hosts and Port sections defined</span>}
-        </FormGroup>
-      </>
+            icon={<TrashIcon />}
+            style={{ padding: '0 40%' }}
+            onClick={() => this.props.onRemoveServer(this.props.index)}
+          />
+        </Td>
+      </Tr>
     );
   }
 }
diff --git a/frontend/src/pages/IstioConfigNew/GatewayForm/ServerList.tsx b/frontend/src/pages/IstioConfigNew/GatewayForm/ServerList.tsx
index ee4bd4239..12fa7dc38 100644
--- a/frontend/src/pages/IstioConfigNew/GatewayForm/ServerList.tsx
+++ b/frontend/src/pages/IstioConfigNew/GatewayForm/ServerList.tsx
@@ -1,12 +1,16 @@
 import * as React from 'react';
-import { Server } from '../../../types/IstioObjects';
-import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table';
+import { Server, ServerForm } from '../../../types/IstioObjects';
+import { cellWidth, Tbody, Td, Th, Thead, Tr, TableComposable } from '@patternfly/react-table';
 import { style } from 'typestyle';
 import { PFColors } from '../../../components/Pf/PfColors';
+import { Button, ButtonVariant } from '@patternfly/react-core';
+import { PlusCircleIcon } from '@patternfly/react-icons';
+import ServerBuilder, { protocols } from './ServerBuilder';
 
 type Props = {
   serverList: Server[];
-  onRemoveServer: (index: number) => void;
+  serverForm: ServerForm[];
+  onChange: (server: Server[], serverForm: ServerForm[]) => void;
 };
 
 const noServerStyle = style({
@@ -16,19 +20,9 @@ const noServerStyle = style({
   width: '100%'
 });
 
-const headerCells: ICell[] = [
+const headerCells = [
   {
-    title: 'Server Hosts',
-    transforms: [cellWidth(100) as any],
-    props: {}
-  },
-  {
-    title: 'Port',
-    transforms: [cellWidth(20) as any],
-    props: {}
-  },
-  {
-    title: 'TLS',
+    title: 'Servers',
     transforms: [cellWidth(100) as any],
     props: {}
   },
@@ -38,70 +32,119 @@ const headerCells: ICell[] = [
   }
 ];
 
+const addServerStyle = style({
+  marginLeft: 0,
+  paddingLeft: 0
+});
+
 class ServerList extends React.Component<Props> {
-  rows = () => {
-    return this.props.serverList.map((server, i) => {
-      return {
-        key: 'server_' + i,
-        cells: [
-          <>
-            {server.hosts.map(host => (
-              <div>{host}</div>
-            ))}
-          </>,
-          <>
-            <div>{server.port.name}</div>
-            <div>
-              [{server.port.number}, {server.port.protocol}]
-            </div>
-          </>,
-          <>
-            {server.tls ? (
-              <>
-                <div>{server.tls.mode}</div>
-                {server.tls.serverCertificate && server.tls.serverCertificate.length > 0 ? (
-                  <div>[{server.tls.serverCertificate}]</div>
-                ) : undefined}
-                {server.tls.privateKey && server.tls.privateKey.length > 0 ? (
-                  <div>[{server.tls.privateKey}]</div>
-                ) : undefined}
-                {server.tls.caCertificates && server.tls.caCertificates.length > 0 ? (
-                  <div>[{server.tls.caCertificates}]</div>
-                ) : undefined}
-              </>
-            ) : undefined}
-          </>,
-          <></>
-        ]
-      };
-    });
-  };
+  onAddServer = () => {
+    const newServerForm: ServerForm = {
+      hosts: [],
+      number: '',
+      protocol: protocols[0],
+      name: '',
+      tlsMode: '',
+      tlsServerCertificate: '',
+      tlsPrivateKey: '',
+      tlsCaCertificate: ''
+    };
+    const sf = this.props.serverForm;
+    sf.push(newServerForm);
 
-  // @ts-ignore
-  actionResolver = (rowData, { rowIndex }) => {
-    const removeAction = {
-      title: 'Remove Rule',
-      // @ts-ignore
-      onClick: (event, rowIndex, rowData, extraData) => {
-        this.props.onRemoveServer(rowIndex);
+    const newServer: Server = {
+      hosts: [],
+      port: {
+        number: 70000,
+        name: '',
+        protocol: 'HTTP'
       }
     };
-    return [removeAction];
+    const s = this.props.serverList;
+    s.push(newServer);
+
+    this.setState({}, () => this.props.onChange(s, sf));
+  };
+
+  onRemoveServer = (index: number) => {
+    const serverList = this.props.serverList;
+    serverList.splice(index, 1);
+
+    const serverForm = this.props.serverForm;
+    serverForm.splice(index, 1);
+
+    this.setState({}, () => this.props.onChange(serverList, serverForm));
+  };
+
+  onChange = (serverForm: ServerForm, i: number) => {
+    const serversForm = this.props.serverForm;
+    serversForm[i] = serverForm;
+
+    const servers = this.props.serverList;
+    const newServer = this.createNewServer(serverForm);
+    if (typeof newServer !== 'undefined') {
+      servers[i] = newServer;
+    }
+
+    this.props.onChange(servers, serversForm);
+  };
+
+  createNewServer = (serverForm: ServerForm) => {
+    if (serverForm.hosts.length === 0) return;
+    if (serverForm.number.length === 0 || isNaN(Number(serverForm.number))) return;
+    if (serverForm.name.length === 0) return;
+
+    const server: Server = {
+      hosts: serverForm.hosts,
+      port: { number: Number(serverForm.number), name: serverForm.name, protocol: serverForm.protocol },
+      tls:
+        serverForm.protocol === 'HTTPS' || serverForm.protocol === 'TLS'
+          ? {
+              mode: serverForm.tlsMode,
+              serverCertificate: serverForm.tlsServerCertificate,
+              privateKey: serverForm.tlsPrivateKey,
+              caCertificates: serverForm.tlsCaCertificate
+            }
+          : undefined
+    };
+    return server;
   };
 
   render() {
     return (
       <>
-        <Table
-          aria-label="Server List"
-          cells={headerCells}
-          rows={this.rows()}
-          // @ts-ignore
-          actionResolver={this.actionResolver}
-        >
-          <TableHeader />
-          <TableBody />
-        </Table>
+        <TableComposable aria-label="Server List">
+          <Thead>
+            <Tr>
+              {headerCells.map(e => (
+                <Th>{e.title}</Th>
+              ))}
+            </Tr>
+          </Thead>
+          <Tbody>
+            {this.props.serverForm.map((server, i) => (
+              <ServerBuilder
+                server={server}
+                onRemoveServer={this.onRemoveServer}
+                index={i}
+                onChange={this.onChange}
+              ></ServerBuilder>
+            ))}
+            <Tr>
+              <Td>
+                <Button
+                  name="addServer"
+                  variant={ButtonVariant.link}
+                  icon={<PlusCircleIcon />}
+                  onClick={this.onAddServer}
+                  className={addServerStyle}
+                >
+                  Add Server to Servers List
+                </Button>
+              </Td>
+            </Tr>
+          </Tbody>
+        </TableComposable>
         {this.props.serverList.length === 0 && <div className={noServerStyle}>No Servers defined</div>}
       </>
     );
diff --git a/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx b/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx
index 5b89eb7df..be42bb9da 100644
--- a/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx
+++ b/frontend/src/pages/IstioConfigNew/IstioConfigNewPage.tsx
@@ -7,7 +7,13 @@ import { ActionGroup, Button, ButtonVariant, Form, FormGroup, TextInput } from '
 import { RenderContent } from '../../components/Nav/Page';
 import { style } from 'typestyle';
 import GatewayForm, { GATEWAY, GATEWAYS, GatewayState, initGateway, isGatewayStateValid } from './GatewayForm';
-import K8sGatewayForm, { K8SGATEWAY, K8SGATEWAYS, K8sGatewayState, initK8sGateway, isK8sGatewayStateValid } from './K8sGatewayForm';
+import K8sGatewayForm, {
+  K8SGATEWAY,
+  K8SGATEWAYS,
+  K8sGatewayState,
+  initK8sGateway,
+  isK8sGatewayStateValid
+} from './K8sGatewayForm';
 import SidecarForm, { initSidecar, isSidecarStateValid, SIDECAR, SIDECARS, SidecarState } from './SidecarForm';
 import { Paths, serverConfig } from '../../config';
 import { PromisesRegistry } from '../../utils/CancelablePromises';
@@ -435,7 +441,12 @@ class IstioConfigNewPage extends React.Component<Props, State> {
               <SidecarForm sidecar={this.state.sidecar} onChange={this.onChangeSidecar} />
             )}
             <ActionGroup>
-              <Button variant={ButtonVariant.primary} isDisabled={!isFormValid} onClick={() => this.showPreview()}>
+              <Button
+                variant={ButtonVariant.primary}
+                isDisabled={!isFormValid}
+                onClick={() => this.showPreview()}
+                data-test={'preview'}
+              >
                 Preview
               </Button>
               <Button variant={ButtonVariant.secondary} onClick={() => this.backToList()}>
diff --git a/frontend/src/pages/IstioConfigNew/K8sGatewayForm.tsx b/frontend/src/pages/IstioConfigNew/K8sGatewayForm.tsx
index fc7414a3e..943b3f232 100644
--- a/frontend/src/pages/IstioConfigNew/K8sGatewayForm.tsx
+++ b/frontend/src/pages/IstioConfigNew/K8sGatewayForm.tsx
@@ -1,12 +1,11 @@
 import * as React from 'react';
 // Use TextInputBase like workaround while PF4 team work in https://github.com/patternfly/patternfly-react/issues/4072
-import { FormGroup, TextInputBase as TextInput } from '@patternfly/react-core';
-import ListenerBuilder from "./GatewayForm/ListenerBuilder";
-import AddressBuilder from "./GatewayForm/AddressBuilder";
-import ListenerList from "./GatewayForm/ListenerList";
-import AddressList from "./GatewayForm/AddressList";
-import {Address, Listener} from '../../types/IstioObjects';
-import { isValid } from 'utils/Common';
+import { FormGroup } from '@patternfly/react-core';
+import AddressList from './GatewayForm/AddressList';
+import { Address, Listener } from '../../types/IstioObjects';
+import ListenerList from './GatewayForm/ListenerList';
+import { isValidHostname, isValidName } from './GatewayForm/ListenerBuilder';
+import { isValidAddress } from './GatewayForm/AddressBuilder';
 
 export const K8SGATEWAY = 'K8sGateway';
 export const K8SGATEWAYS = 'k8sgateways';
@@ -18,36 +17,52 @@ type Props = {
 
 // Gateway and Sidecar states are consolidated in the parent page
 export type K8sGatewayState = {
-  workloadSelectorValid: boolean;
-  workloadSelectorLabels: string;
   listeners: Listener[];
   addresses: Address[];
-  addListener: Listener;
-  addAddress: Address;
   validHosts: boolean;
+  listenersForm: ListenerForm[];
 };
 
 export const initK8sGateway = (): K8sGatewayState => ({
-  workloadSelectorLabels: 'app=gatewayapi',
-  workloadSelectorValid: true,
   listeners: [],
   addresses: [],
-  addListener: {
-    hostname: '',
-    port: 80,
-    name: 'default',
-    protocol: 'HTTP',
-    allowedRoutes: {namespaces: {from: "Same", selector: {matchLabels: {}}}}
-  },
-  addAddress: {
-    type: 'IPAddress',
-    value: '',
-  },
-  validHosts: false
+  validHosts: false,
+  listenersForm: []
 });
 
 export const isK8sGatewayStateValid = (g: K8sGatewayState): boolean => {
-  return g.workloadSelectorValid && g.listeners.length > 0;
+  return (
+    g.listeners.length > 0 && validListeners(g.listeners) && (g.addresses.length === 0 || validAddresses(g.addresses))
+  );
+};
+
+export type ListenerForm = {
+  isHostValid: boolean;
+  hostname: string;
+  port: string;
+  name: string;
+  protocol: string;
+  from: string;
+  isLabelSelectorValid: boolean;
+  sSelectorLabels: string;
+};
+
+const validListeners = (listeners: Listener[]) => {
+  return listeners.every((e, _) => {
+    return (
+      isValidName(e.name) &&
+      typeof e.port !== 'undefined' &&
+      e.port >= 0 &&
+      e.port <= 65535 &&
+      isValidHostname(e.hostname)
+    );
+  });
+};
+
+const validAddresses = (address: Address[]) => {
+  return address.every((a, _) => {
+    return isValidAddress(a);
+  });
 };
 
 class K8sGatewayForm extends React.Component<Props, K8sGatewayState> {
@@ -60,148 +75,26 @@ class K8sGatewayForm extends React.Component<Props, K8sGatewayState> {
     this.setState(this.props.k8sGateway);
   }
 
-  addWorkloadLabels = (value: string, _) => {
-    if (value.length === 0) {
-      this.setState(
-        {
-          workloadSelectorValid: false,
-          workloadSelectorLabels: ''
-        },
-        () => this.props.onChange(this.state)
-      );
-      return;
-    }
-    value = value.trim();
-    const labels: string[] = value.split(',');
-    let isValid = true;
-    // Some smoke validation rules for the labels
-    for (let i = 0; i < labels.length; i++) {
-      const label = labels[i];
-      if (label.indexOf('=') < 0) {
-        isValid = false;
-        break;
-      }
-      const splitLabel: string[] = label.split('=');
-      if (splitLabel.length !== 2) {
-        isValid = false;
-        break;
-      }
-      if (splitLabel[0].trim().length === 0 || splitLabel[1].trim().length === 0) {
-        isValid = false;
-        break;
-      }
-    }
-    this.setState(
-      {
-        workloadSelectorValid: isValid,
-        workloadSelectorLabels: value
-      },
-      () => this.props.onChange(this.state)
-    );
-  };
-
-  onAddListener = () => {
-    this.setState(
-      prevState => {
-        prevState.listeners.push(prevState.addListener);
-        return {
-          listeners: prevState.listeners,
-          addListener: {
-            hostname: '',
-            port: 80,
-            name: 'http',
-            protocol: 'HTTP',
-            allowedRoutes: {namespaces: {from: "Same", selector: {matchLabels: {}}}}
-          }
-        };
-      },
-      () => this.props.onChange(this.state)
-    );
-  };
-
-  onRemoveListener = (index: number) => {
-    this.setState(
-      prevState => {
-        prevState.listeners.splice(index, 1);
-        return {
-          listeners: prevState.listeners
-        };
-      },
-      () => this.props.onChange(this.state)
-    );
+  onChangeListener = (listeners: Listener[], listenersForm: ListenerForm[]) => {
+    this.setState({ listeners: listeners, listenersForm: listenersForm }, () => this.props.onChange(this.state));
   };
 
-  onAddAddress = () => {
-    this.setState(
-      prevState => {
-        prevState.addresses.push(prevState.addAddress);
-        return {
-          addresses: prevState.addresses,
-          addAddress: {
-            type: 'IPAddress',
-            value: '',
-          }
-        };
-      },
-      () => this.props.onChange(this.state)
-    );
-  };
-
-  onRemoveAddress = (index: number) => {
-    this.setState(
-      prevState => {
-        prevState.addresses.splice(index, 1);
-        return {
-          addresses: prevState.addresses
-        };
-      },
-      () => this.props.onChange(this.state)
-    );
+  onChangeAddress = (addresses: Address[]) => {
+    this.setState({ addresses: addresses }, () => this.props.onChange(this.state));
   };
 
   render() {
     return (
       <>
-        <FormGroup
-          fieldId="workloadLabels"
-          label="Labels"
-          helperText="One or more labels to select a workload where the Gateway is applied."
-          helperTextInvalid="Enter a label in the format <label>=<value>. Enter one or multiple labels separated by comma."
-          validated={isValid(this.state.workloadSelectorValid)}
-        >
-          <TextInput
-            id="gwLabels"
-            name="gwLabels"
-            value={this.state.workloadSelectorLabels}
-            onChange={this.addWorkloadLabels}
-            validated={isValid(this.state.workloadSelectorValid)}
-          />
-        </FormGroup>
         <FormGroup label="Listeners" fieldId="listener" isRequired={true}>
-          <ListenerBuilder
-            onAddListener={listener => {
-              this.setState(
-                {
-                  addListener: listener
-                },
-                () => this.onAddListener()
-              );
-            }}
+          <ListenerList
+            onChange={this.onChangeListener}
+            listenersForm={this.state.listenersForm}
+            listeners={this.state.listeners}
           />
-          <ListenerList listenerList={this.state.listeners} onRemoveListener={this.onRemoveListener} />
         </FormGroup>
         <FormGroup label="Addresses" fieldId="gwAddressList">
-          <AddressBuilder
-            onAddAddress={address => {
-              this.setState(
-                {
-                  addAddress: address
-                },
-                () => this.onAddAddress()
-              );
-            }}
-          />
-          <AddressList addressList={this.state.addresses} onRemoveAddress={this.onRemoveAddress} />
+          <AddressList onChange={this.onChangeAddress} addressList={this.state.addresses} />
         </FormGroup>
       </>
     );
diff --git a/frontend/src/pages/IstioConfigNew/ServiceEntryForm.tsx b/frontend/src/pages/IstioConfigNew/ServiceEntryForm.tsx
index 49a4bf741..abaeffee7 100644
--- a/frontend/src/pages/IstioConfigNew/ServiceEntryForm.tsx
+++ b/frontend/src/pages/IstioConfigNew/ServiceEntryForm.tsx
@@ -4,10 +4,11 @@ import { Button, ButtonVariant, FormGroup, FormSelect, FormSelectOption } from '
 import { TextInputBase as TextInput } from '@patternfly/react-core/dist/js/components/TextInput/TextInput';
 import { isGatewayHostValid } from '../../utils/IstioConfigUtils';
 import { cellWidth, ICell, Table, TableBody, TableHeader } from '@patternfly/react-table';
-import { PlusCircleIcon } from '@patternfly/react-icons';
+import { PlusCircleIcon, TrashIcon } from '@patternfly/react-icons';
 import { style } from 'typestyle';
 import { PFColors } from '../../components/Pf/PfColors';
 import { isValid } from 'utils/Common';
+import { FormEvent } from 'react';
 
 export const SERVICE_ENTRY = 'ServiceEntry';
 export const SERVICE_ENTRIES = 'serviceentries';
@@ -62,29 +63,60 @@ type Props = {
   onChange: (serviceEntry: ServiceEntryState) => void;
 };
 
+type FormPort = {
+  number: string;
+  name: string;
+  protocol: string;
+  targetPort: string;
+};
+
 export type ServiceEntryState = {
   serviceEntry: ServiceEntrySpec;
   validHosts: boolean;
-  addNewPortNumber: string;
-  addNewPortName: string;
-  addNewPortProtocol: string;
-  addNewTargetPort: string;
+  formPorts: FormPort[];
 };
 
 export const initServiceEntry = (): ServiceEntryState => ({
   serviceEntry: {
     location: location[0], // MESH_EXTERNAL
-    resolution: resolution[0] // NONE
+    resolution: resolution[0], // NONE
+    ports: []
   },
   validHosts: false,
-  addNewPortNumber: '80',
-  addNewPortName: '',
-  addNewPortProtocol: protocols[0],
-  addNewTargetPort: ''
+  formPorts: Array<FormPort>()
 });
 
+const isValidPort = (ports: FormPort[]) => {
+  return ports.every(
+    (p, i) =>
+      isValidName(p.name) &&
+      isValidPortNumber(p.number) &&
+      isValidTargetPort(p.targetPort) &&
+      noDuplicatePortNames(p.name, i, ports)
+  );
+};
+
+const noDuplicatePortNames = (name: string, index: number, ports: FormPort[]) => {
+  return ports.every((p, i) => (i !== index ? p.name !== name : true));
+};
+
 export const isServiceEntryValid = (se: ServiceEntryState): boolean => {
-  return se.validHosts && se.serviceEntry.ports !== undefined && se.serviceEntry.ports.length > 0;
+  return se.validHosts && se.serviceEntry.ports !== undefined && isValidPort(se.formPorts);
+};
+
+const isValidName = (name: string): boolean => {
+  return name.length > 0;
+};
+
+const isValidPortNumber = (portNumber: string): boolean => {
+  return portNumber.length > 0 && !isNaN(Number(portNumber)) && Number(portNumber) >= 0 && Number(portNumber) <= 65536;
+};
+
+const isValidTargetPort = (targetPort: string): boolean => {
+  return (
+    targetPort.length === 0 ||
+    (targetPort.length > 0 && !isNaN(Number(targetPort)) && Number(targetPort) >= 0 && Number(targetPort) <= 65536)
+  );
 };
 
 class ServiceEntryForm extends React.Component<Props, ServiceEntryState> {
@@ -93,29 +125,6 @@ class ServiceEntryForm extends React.Component<Props, ServiceEntryState> {
     this.state = initServiceEntry();
   }
 
-  // @ts-ignore
-  actionResolver = (rowData, { rowIndex }) => {
-    const removeAction = {
-      title: 'Remove Port',
-      // @ts-ignore
-      onClick: (event, rowIndex, rowData, extraData) => {
-        this.setState(
-          prevState => {
-            prevState.serviceEntry.ports?.splice(rowIndex, 1);
-            return {
-              serviceEntry: prevState.serviceEntry
-            };
-          },
-          () => this.props.onChange(this.state)
-        );
-      }
-    };
-    if (this.state.serviceEntry.ports && rowIndex < this.state.serviceEntry.ports.length) {
-      return [removeAction];
-    }
-    return [];
-  };
-
   areValidHosts = (hosts: string[]): boolean => {
     if (hosts.length === 0) {
       return false;
@@ -130,15 +139,6 @@ class ServiceEntryForm extends React.Component<Props, ServiceEntryState> {
     return isValid;
   };
 
-  isValidPort = () => {
-    const validPortNumber = this.state.addNewPortNumber.length > 0 && !isNaN(Number(this.state.addNewPortNumber));
-    const validPortName = this.state.addNewPortName.length > 0;
-    const validTargetPort =
-      this.state.addNewTargetPort.length === 0 ||
-      (this.state.addNewTargetPort.length > 0 && !isNaN(Number(this.state.addNewTargetPort)));
-    return validPortNumber && validPortName && validTargetPort;
-  };
-
   onAddHosts = (value: string, _) => {
     const hosts = value.trim().length === 0 ? [] : value.split(',').map(host => host.trim());
     this.setState(
@@ -177,123 +177,203 @@ class ServiceEntryForm extends React.Component<Props, ServiceEntryState> {
     );
   };
 
-  onAddPortNumber = (value: string, _) => {
-    this.setState({
-      addNewPortNumber: value
-    });
+  onAddPortNumber = (value: string, e: FormEvent) => {
+    const formPorts = this.state.formPorts;
+    const eName = e.currentTarget.getAttribute('name') !== null ? e.currentTarget.getAttribute('name') : '0';
+    // @ts-ignore
+    const i = parseInt(eName);
+    formPorts[i].number = value;
+
+    // service entry
+    const se = this.checkDefined(i);
+    if (!isNaN(parseInt(value)) && se.ports !== undefined) {
+      se.ports[i].number = parseInt(value);
+    }
+
+    this.setState(
+      {
+        formPorts: formPorts,
+        serviceEntry: se
+      },
+      () => this.props.onChange(this.state)
+    );
+  };
+
+  onAddPortName = (value: string, e: FormEvent) => {
+    const formPorts = this.state.formPorts;
+    const eName = e.currentTarget.getAttribute('name') !== null ? e.currentTarget.getAttribute('name') : '0';
+    // @ts-ignore
+    const i = parseInt(eName);
+    formPorts[i].name = value;
+
+    // service entry
+    const se = this.checkDefined(i);
+    if (se.ports !== undefined) {
+      se.ports[i].name = value;
+    }
+
+    this.setState(
+      {
+        formPorts: formPorts,
+        serviceEntry: se
+      },
+      () => this.props.onChange(this.state)
+    );
   };
 
-  onAddPortName = (value: string, _) => {
-    this.setState({
-      addNewPortName: value
-    });
+  onAddPortProtocol = (value: string, e: FormEvent) => {
+    const formPorts = this.state.formPorts;
+    const eName = e.currentTarget.getAttribute('name') !== null ? e.currentTarget.getAttribute('name') : '0';
+    // @ts-ignore
+    const i = parseInt(eName);
+    formPorts[i].protocol = value;
+
+    // service entry
+    const se = this.checkDefined(i);
+    if (se.ports !== undefined) {
+      se.ports[i].protocol = value;
+    }
+
+    this.setState(
+      {
+        formPorts: formPorts,
+        serviceEntry: se
+      },
+      () => this.props.onChange(this.state)
+    );
   };
 
-  onAddPortProtocol = (value: string, _) => {
-    this.setState({
-      addNewPortProtocol: value
-    });
+  onAddTargetPort = (value: string, e: FormEvent) => {
+    const formPorts = this.state.formPorts;
+    const eName = e.currentTarget.getAttribute('name') !== null ? e.currentTarget.getAttribute('name') : '0';
+    // @ts-ignore
+    const i = parseInt(eName);
+    formPorts[i].targetPort = value;
+
+    // service entry
+    const se = this.checkDefined(i);
+    if (!isNaN(parseInt(value)) && se.ports !== undefined) {
+      se.ports[i].targetPort = parseInt(value);
+    }
+
+    this.setState(
+      {
+        formPorts: formPorts,
+        serviceEntry: se
+      },
+      () => this.props.onChange(this.state)
+    );
   };
 
-  onAddTargetPort = (value: string, _) => {
-    this.setState({
-      addNewTargetPort: value
-    });
+  checkDefined = (index: number) => {
+    const se = this.state.serviceEntry;
+    if (typeof se.ports !== 'undefined' && typeof se.ports[index] === 'undefined') {
+      const np: Port = {
+        name: '',
+        protocol: protocols[0],
+        number: 0
+      };
+      se.ports.splice(index, 0, np);
+    }
+    return se;
   };
 
   onAddNewPort = () => {
-    // @ts-ignore
+    const newPort: FormPort = {
+      name: '',
+      protocol: protocols[0],
+      number: '',
+      targetPort: ''
+    };
+    const newports = this.state.formPorts;
+    newports.push(newPort);
+
     this.setState(
-      prevState => {
-        const newPort: Port = {
-          number: +this.state.addNewPortNumber,
-          name: this.state.addNewPortName,
-          protocol: this.state.addNewPortProtocol
-        };
-        if (this.state.addNewTargetPort.length > 0) {
-          newPort.targetPort = +this.state.addNewTargetPort;
-        }
-        if (!prevState.serviceEntry.ports) {
-          prevState.serviceEntry.ports = [];
-        }
-        prevState.serviceEntry.ports.push(newPort);
-        return {
-          serviceEntry: prevState.serviceEntry,
-          addNewPortNumber: '80',
-          addNewPortName: '',
-          addNewPortProtocol: protocols[0],
-          addNewTargetPort: ''
-        };
+      {
+        formPorts: newports
       },
       () => this.props.onChange(this.state)
     );
   };
 
+  handleDelete = (_: any, index: number) => {
+    const state = this.state.formPorts;
+    state.splice(index, 1);
+    const se = this.state.serviceEntry;
+    se.ports?.splice(index, 1);
+
+    this.setState({ formPorts: state, serviceEntry: se }, () => this.props.onChange(this.state));
+  };
+
   rows() {
-    return (this.state.serviceEntry.ports || [])
+    return (this.state.formPorts || [])
       .map((p, i) => ({
-        key: 'port_' + i,
-        cells: [<>{p.number}</>, <>{p.name}</>, <>{p.protocol}</>, <>{p.targetPort}</>, '']
+        key: 'portNew' + i,
+        cells: [
+          <>
+            <TextInput
+              value={p.number}
+              id="addPortNumber"
+              aria-describedby="add port number"
+              name={i.toString()}
+              placeholder="80"
+              onChange={this.onAddPortNumber}
+              validated={isValid(isValidPortNumber(p.number))}
+            />
+          </>,
+          <>
+            <TextInput
+              value={p.name}
+              id="addPortName"
+              aria-describedby="add port name"
+              name={i.toString()}
+              onChange={this.onAddPortName}
+              validated={isValid(isValidName(p.name) && noDuplicatePortNames(p.name, i, this.state.formPorts))}
+            />
+          </>,
+          <>
+            <FormSelect value={p.protocol} id="addPortProtocol" name={i.toString()} onChange={this.onAddPortProtocol}>
+              {protocols.map((option, index) => (
+                <FormSelectOption key={'p' + index} value={option} label={option} />
+              ))}
+            </FormSelect>
+          </>,
+          <>
+            <TextInput
+              value={p.targetPort}
+              id="addTargetPort"
+              aria-describedby="add target port"
+              name={i.toString()}
+              onChange={this.onAddTargetPort}
+              validated={isValid(isValidTargetPort(p.targetPort))}
+            />
+          </>,
+          <>
+            <Button
+              id="deleteBtn"
+              variant={ButtonVariant.link}
+              icon={<TrashIcon />}
+              style={{ padding: 0 }}
+              onClick={e => this.handleDelete(e, i)}
+            />
+          </>
+        ]
       }))
       .concat([
         {
           key: 'portNew',
           cells: [
-            <>
-              <TextInput
-                value={this.state.addNewPortNumber}
-                id="addPortNumber"
-                aria-describedby="add port number"
-                name="addPortNumber"
-                onChange={this.onAddPortNumber}
-                validated={isValid(
-                  this.state.addNewPortNumber.length > 0 && !isNaN(Number(this.state.addNewPortNumber))
-                )}
-              />
-            </>,
-            <>
-              <TextInput
-                value={this.state.addNewPortName}
-                id="addPortName"
-                aria-describedby="add port name"
-                name="addPortName"
-                onChange={this.onAddPortName}
-                validated={isValid(this.state.addNewPortName.length > 0)}
-              />
-            </>,
-            <>
-              <FormSelect
-                value={this.state.addNewPortProtocol}
-                id="addPortProtocol"
-                name="addPortProtocol"
-                onChange={this.onAddPortProtocol}
-              >
-                {protocols.map((option, index) => (
-                  <FormSelectOption key={'p' + index} value={option} label={option} />
-                ))}
-              </FormSelect>
-            </>,
-            <>
-              <TextInput
-                value={this.state.addNewTargetPort}
-                id="addTargetPort"
-                aria-describedby="add target port"
-                name="addTargetPort"
-                onChange={this.onAddTargetPort}
-                validated={isValid(
-                  this.state.addNewTargetPort.length === 0 ||
-                    (this.state.addNewTargetPort.length > 0 && !isNaN(Number(this.state.addNewTargetPort)))
-                )}
-              />
-            </>,
             <>
               <Button
                 id="addServerBtn"
                 variant={ButtonVariant.link}
                 icon={<PlusCircleIcon />}
-                isDisabled={!this.isValidPort()}
+                style={{ padding: 0 }}
                 onClick={this.onAddNewPort}
-              />
+              >
+                {' '}
+                Add Port
+              </Button>
             </>
           ]
         }
@@ -334,14 +414,8 @@ class ServiceEntryForm extends React.Component<Props, ServiceEntryState> {
             ))}
           </FormSelect>
         </FormGroup>
-        <FormGroup label="Ports" fieldId="ports">
-          <Table
-            aria-label="Ports"
-            cells={headerCells}
-            rows={this.rows()}
-            // @ts-ignore
-            actionResolver={this.actionResolver}
-          >
+        <FormGroup label="Ports" fieldId="ports" isRequired={true}>
+          <Table aria-label="Ports" cells={headerCells} rows={this.rows()}>
             <TableHeader />
             <TableBody />
           </Table>
diff --git a/frontend/src/pages/Login/LoginPage.tsx b/frontend/src/pages/Login/LoginPage.tsx
index 502c01792..53c24a05f 100644
--- a/frontend/src/pages/Login/LoginPage.tsx
+++ b/frontend/src/pages/Login/LoginPage.tsx
@@ -120,15 +120,11 @@ export class LoginPage extends React.Component<LoginProps, LoginState> {
     // Reference: https://openid.net/specs/openid-connect-core-1_0-final.html#AuthError
     if (urlParams.get('error')) {
       if (urlParams.get('error_description')) {
-        console.warn(`Authentication error_description: ${urlParams.get('error_description')}`)
-        messagesArray.push(
-          this.renderMessage(`Authentication failed!`, 'danger', 'idp-err')
-        );
+        console.warn(`Authentication error_description: ${urlParams.get('error_description')}`);
+        messagesArray.push(this.renderMessage(`Authentication failed!`, 'danger', 'idp-err'));
       } else {
-        console.warn(`Authentication error: ${urlParams.get('error')}`)
-        messagesArray.push(
-          this.renderMessage(`Authentication failed.`, 'danger', 'idp-err')
-        );
+        console.warn(`Authentication error: ${urlParams.get('error')}`);
+        messagesArray.push(this.renderMessage(`Authentication failed.`, 'danger', 'idp-err'));
       }
     }
   };
@@ -166,10 +162,8 @@ export class LoginPage extends React.Component<LoginProps, LoginState> {
     // The backend will set an "openid_error" url parameter when there is some failure.
     // Only log the openid_error since we cannot guarantee it is not spoofed. We only show a generic error message in the UI.
     if (urlParams.get('openid_error')) {
-      console.warn(`Authentication openid_error: ${urlParams.get('openid_error')}`)
-      messages.push(
-        this.renderMessage(`OpenID authentication failed.`, 'danger', 'openid-err')
-      );
+      console.warn(`Authentication openid_error: ${urlParams.get('openid_error')}`);
+      messages.push(this.renderMessage(`OpenID authentication failed.`, 'danger', 'openid-err'));
     }
 
     return messages;
@@ -193,8 +187,14 @@ export class LoginPage extends React.Component<LoginProps, LoginState> {
       isHash = pattern.test(window.location.hash);
     }
 
-    if ((authenticationConfig.strategy === AuthStrategy.openshift|| authenticationConfig.strategy === AuthStrategy.openid)
-      && !isHash && this.props.status === LoginStatus.loggedOut && messages.length === 0 && (this.props.message ?? '').length === 0) {
+    if (
+      (authenticationConfig.strategy === AuthStrategy.openshift ||
+        authenticationConfig.strategy === AuthStrategy.openid) &&
+      !isHash &&
+      this.props.status === LoginStatus.loggedOut &&
+      messages.length === 0 &&
+      (this.props.message ?? '').length === 0
+    ) {
       window.location.href = authenticationConfig.authorizationEndpoint!;
     }
 
diff --git a/frontend/src/pages/Overview/CanaryUpgradeProgress.tsx b/frontend/src/pages/Overview/CanaryUpgradeProgress.tsx
index bc3474da8..7503318ea 100644
--- a/frontend/src/pages/Overview/CanaryUpgradeProgress.tsx
+++ b/frontend/src/pages/Overview/CanaryUpgradeProgress.tsx
@@ -6,49 +6,52 @@ import { CanaryUpgradeStatus } from 'types/IstioObjects';
 import { style } from 'typestyle';
 
 type Props = {
-    canaryUpgradeStatus: CanaryUpgradeStatus;
+  canaryUpgradeStatus: CanaryUpgradeStatus;
 };
 
 export const infoStyle = style({
-    margin: '0px 0px -1px 4px'
+  margin: '0px 0px -1px 4px'
 });
 
-
 class CanaryUpgradeProgress extends React.Component<Props> {
-    render() {
-        const total = this.props.canaryUpgradeStatus.migratedNamespaces.length + this.props.canaryUpgradeStatus.pendingNamespaces.length;
-        const migrated = (total > 0) ? this.props.canaryUpgradeStatus.migratedNamespaces.length * 100 / total : 0;
+  render() {
+    const total =
+      this.props.canaryUpgradeStatus.migratedNamespaces.length +
+      this.props.canaryUpgradeStatus.pendingNamespaces.length;
+    const migrated = total > 0 ? (this.props.canaryUpgradeStatus.migratedNamespaces.length * 100) / total : 0;
 
-        return (
-            <div style={{ textAlign: 'center' }} data-test="canary-upgrade">
-                <div>
-                    <div>
-                        Canary upgrade status
-                        <Tooltip
-                            position={TooltipPosition.right}
-                            content={`There is an in progress canary upgrade from version "${this.props.canaryUpgradeStatus.currentVersion}" to version "${this.props.canaryUpgradeStatus.upgradeVersion}"`}
-                        >
-                            <KialiIcon.Info className={infoStyle} />
-                        </Tooltip>
-                    </div>
-                    <div style={{ height: 180 }}>
-                        <ChartDonutUtilization
-                            ariaDesc="Canary upgrade status"
-                            ariaTitle="Canary upgrade status"
-                            constrainToVisibleArea
-                            data={{ x: 'Migrated namespaces', y: migrated }}
-                            labels={({ datum }) => datum.x ? `${datum.x}: ${datum.y.toFixed(2)}%` : null}
-                            invert
-                            title={`${migrated.toFixed(2)}%`}
-                            height={170}
-                            themeColor={ChartThemeColor.green}
-                        />
-                    </div>
-                    <div><p>{`${this.props.canaryUpgradeStatus.migratedNamespaces.length} of ${total} namespaces migrated`}</p></div>
-                </div>
-            </div>
-        );
-    }
+    return (
+      <div style={{ textAlign: 'center' }} data-test="canary-upgrade">
+        <div>
+          <div>
+            Canary upgrade status
+            <Tooltip
+              position={TooltipPosition.right}
+              content={`There is an in progress canary upgrade from version "${this.props.canaryUpgradeStatus.currentVersion}" to version "${this.props.canaryUpgradeStatus.upgradeVersion}"`}
+            >
+              <KialiIcon.Info className={infoStyle} />
+            </Tooltip>
+          </div>
+          <div style={{ height: 180 }}>
+            <ChartDonutUtilization
+              ariaDesc="Canary upgrade status"
+              ariaTitle="Canary upgrade status"
+              constrainToVisibleArea
+              data={{ x: 'Migrated namespaces', y: migrated }}
+              labels={({ datum }) => (datum.x ? `${datum.x}: ${datum.y.toFixed(2)}%` : null)}
+              invert
+              title={`${migrated.toFixed(2)}%`}
+              height={170}
+              themeColor={ChartThemeColor.green}
+            />
+          </div>
+          <div>
+            <p>{`${this.props.canaryUpgradeStatus.migratedNamespaces.length} of ${total} namespaces migrated`}</p>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default CanaryUpgradeProgress;
diff --git a/frontend/src/pages/Overview/ControlPlaneBadge.tsx b/frontend/src/pages/Overview/ControlPlaneBadge.tsx
index 7079b5e34..a9aa97d4e 100644
--- a/frontend/src/pages/Overview/ControlPlaneBadge.tsx
+++ b/frontend/src/pages/Overview/ControlPlaneBadge.tsx
@@ -1,16 +1,18 @@
 import { Label } from '@patternfly/react-core';
 import * as React from 'react';
-import IstioStatusInline from "../../components/IstioStatus/IstioStatusInline";
+import IstioStatusInline from '../../components/IstioStatus/IstioStatusInline';
 
 class ControlPlaneBadge extends React.Component<{}> {
-    render() {
-        return (
-            <>
-              <Label style={{ marginLeft: 5 }} color="green" isCompact>Control plane</Label>
-              {' '} <IstioStatusInline />
-            </>
-        );
-    }
+  render() {
+    return (
+      <>
+        <Label style={{ marginLeft: 5 }} color="green" isCompact>
+          Control plane
+        </Label>{' '}
+        <IstioStatusInline />
+      </>
+    );
+  }
 }
 
 export default ControlPlaneBadge;
diff --git a/frontend/src/pages/Overview/ControlPlaneNamespaceStatus.tsx b/frontend/src/pages/Overview/ControlPlaneNamespaceStatus.tsx
index 0beafd3ca..eb02dfc99 100644
--- a/frontend/src/pages/Overview/ControlPlaneNamespaceStatus.tsx
+++ b/frontend/src/pages/Overview/ControlPlaneNamespaceStatus.tsx
@@ -6,54 +6,70 @@ import NamespaceInfo from './NamespaceInfo';
 import { infoStyle } from './OverviewCardControlPlaneNamespace';
 
 type Props = {
-    namespace: NamespaceInfo
-    outboundTrafficPolicy?: OutboundTrafficPolicy
+  namespace: NamespaceInfo;
+  outboundTrafficPolicy?: OutboundTrafficPolicy;
 };
 
 class ControlPlaneNamespaceStatus extends React.Component<Props> {
+  render() {
+    let maxProxyPushTime: number | undefined = undefined;
+    if (this.props.namespace.controlPlaneMetrics && this.props.namespace.controlPlaneMetrics.istiod_proxy_time) {
+      maxProxyPushTime =
+        this.props.namespace.controlPlaneMetrics?.istiod_proxy_time[0].datapoints.reduce((a, b) =>
+          a[1] < b[1] ? a : b
+        )[1] * 1000;
+    }
+    let showProxyPushTime = false;
+    if (maxProxyPushTime && !isNaN(maxProxyPushTime)) {
+      showProxyPushTime = true;
+    }
 
-    render() {
-        let maxProxyPushTime: number | undefined = undefined;
-        if (this.props.namespace.controlPlaneMetrics && this.props.namespace.controlPlaneMetrics.istiod_proxy_time) {
-            maxProxyPushTime = this.props.namespace.controlPlaneMetrics?.istiod_proxy_time[0].datapoints.reduce((a, b) => a[1] < b[1] ? a : b)[1] * 1000;
-        }
-        let showProxyPushTime = false;
-        if (maxProxyPushTime && !isNaN(maxProxyPushTime)) {
-            showProxyPushTime = true;
-        }
-
-        return (
-            <div style={{ textAlign: 'left' }}>
-                {this.props.outboundTrafficPolicy &&
-                    <div>
-                        <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Outbound policy</div>
-                        <Tooltip
-                            position={TooltipPosition.right}
-                            content={<div style={{ textAlign: 'left' }}>This value represents the meshConfig.outboundTrafficPolicy.mode, that configures the sidecar handling of external services, that is, those services that are not defined in Istio’s internal service registry. If this option is set to ALLOW_ANY, the Istio proxy lets calls to unknown services pass through. If the option is set to REGISTRY_ONLY, then the Istio proxy blocks any host without an HTTP service or service entry defined within the mesh</div>}
-                        >
-                            <Label isCompact color="blue">
-                                {this.props.outboundTrafficPolicy.mode}<KialiIcon.Info className={infoStyle} />
-                            </Label>
-                        </Tooltip>
-                    </div>
-                }
-                {showProxyPushTime &&
-                    <div>
-                        <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Proxy push time</div>
-                        <Tooltip
-                            position={TooltipPosition.right}
-                            content={<div style={{ textAlign: 'left' }}>This value represents the delay in seconds between config change and a proxy receiving all required configuration.</div>}
-                        >
-                            <Label isCompact color="blue">
-                                {maxProxyPushTime?.toFixed(2)} ms<KialiIcon.Info className={infoStyle} />
-                            </Label>
-
-                        </Tooltip>
-                    </div>
-                }
-            </div>
-        );
-    };
+    return (
+      <div style={{ textAlign: 'left' }}>
+        {this.props.outboundTrafficPolicy && (
+          <div>
+            <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Outbound policy</div>
+            <Tooltip
+              position={TooltipPosition.right}
+              content={
+                <div style={{ textAlign: 'left' }}>
+                  This value represents the meshConfig.outboundTrafficPolicy.mode, that configures the sidecar handling
+                  of external services, that is, those services that are not defined in Istio’s internal service
+                  registry. If this option is set to ALLOW_ANY, the Istio proxy lets calls to unknown services pass
+                  through. If the option is set to REGISTRY_ONLY, then the Istio proxy blocks any host without an HTTP
+                  service or service entry defined within the mesh
+                </div>
+              }
+            >
+              <Label isCompact color="blue">
+                {this.props.outboundTrafficPolicy.mode}
+                <KialiIcon.Info className={infoStyle} />
+              </Label>
+            </Tooltip>
+          </div>
+        )}
+        {showProxyPushTime && (
+          <div>
+            <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Proxy push time</div>
+            <Tooltip
+              position={TooltipPosition.right}
+              content={
+                <div style={{ textAlign: 'left' }}>
+                  This value represents the delay in seconds between config change and a proxy receiving all required
+                  configuration.
+                </div>
+              }
+            >
+              <Label isCompact color="blue">
+                {maxProxyPushTime?.toFixed(2)} ms
+                <KialiIcon.Info className={infoStyle} />
+              </Label>
+            </Tooltip>
+          </div>
+        )}
+      </div>
+    );
+  }
 }
 
 export default ControlPlaneNamespaceStatus;
diff --git a/frontend/src/pages/Overview/ControlPlaneVersionBadge.tsx b/frontend/src/pages/Overview/ControlPlaneVersionBadge.tsx
index 7814faa5a..7cb655190 100644
--- a/frontend/src/pages/Overview/ControlPlaneVersionBadge.tsx
+++ b/frontend/src/pages/Overview/ControlPlaneVersionBadge.tsx
@@ -2,18 +2,20 @@ import { Label } from '@patternfly/react-core';
 import * as React from 'react';
 
 type Props = {
-    version: string;
-    isCanary: boolean;
+  version: string;
+  isCanary: boolean;
 };
 
 class ControlPlaneVersionBadge extends React.Component<Props> {
-    render() {
-        return (
-            <>
-              <Label style={{ marginLeft: 5 }} color={this.props.isCanary ? "blue": "orange"} isCompact>{this.props.version}</Label>
-            </>
-        );
-    }
+  render() {
+    return (
+      <>
+        <Label style={{ marginLeft: 5 }} color={this.props.isCanary ? 'blue' : 'orange'} isCompact>
+          {this.props.version}
+        </Label>
+      </>
+    );
+  }
 }
 
 export default ControlPlaneVersionBadge;
diff --git a/frontend/src/pages/Overview/Filters.ts b/frontend/src/pages/Overview/Filters.ts
index a3b8b8f79..9cc6b244a 100644
--- a/frontend/src/pages/Overview/Filters.ts
+++ b/frontend/src/pages/Overview/Filters.ts
@@ -1,4 +1,10 @@
-import { ActiveFiltersInfo, FILTER_ACTION_APPEND, AllFilterTypes, RunnableFilter, FilterValue } from '../../types/Filters';
+import {
+  ActiveFiltersInfo,
+  FILTER_ACTION_APPEND,
+  AllFilterTypes,
+  RunnableFilter,
+  FilterValue
+} from '../../types/Filters';
 import { DEGRADED, FAILURE, HEALTHY, NOT_READY } from '../../types/Health';
 import { NamespaceInfo } from './NamespaceInfo';
 import { MTLSStatuses } from '../../types/TLSStatus';
diff --git a/frontend/src/pages/Overview/NamespaceStatuses.tsx b/frontend/src/pages/Overview/NamespaceStatuses.tsx
index 8683a804c..1bb54ac8c 100644
--- a/frontend/src/pages/Overview/NamespaceStatuses.tsx
+++ b/frontend/src/pages/Overview/NamespaceStatuses.tsx
@@ -33,7 +33,9 @@ class NamespaceStatuses extends React.Component<Props> {
       <>
         <div style={{ textAlign: 'left' }}>
           <span>
-            <div style={{ display: 'inline-block', width: '125px' }} data-test={'overview-type-' + this.props.type}>{text}</div>
+            <div style={{ display: 'inline-block', width: '125px' }} data-test={'overview-type-' + this.props.type}>
+              {text}
+            </div>
             <div style={{ display: 'inline-block' }}>
               {status.inNotReady.length > 0 && (
                 <OverviewStatus
diff --git a/frontend/src/pages/Overview/OverviewCardControlPlaneNamespace.tsx b/frontend/src/pages/Overview/OverviewCardControlPlaneNamespace.tsx
index 8a68c5bcc..ec07e904a 100644
--- a/frontend/src/pages/Overview/OverviewCardControlPlaneNamespace.tsx
+++ b/frontend/src/pages/Overview/OverviewCardControlPlaneNamespace.tsx
@@ -15,168 +15,193 @@ import { style } from 'typestyle';
 import { IstiodResourceThresholds } from 'types/IstioStatus';
 
 export const infoStyle = style({
-    margin: '0px 0px -1px 4px'
+  margin: '0px 0px -1px 4px'
 });
 
 type ControlPlaneProps = {
-    pilotLatency?: Metric[];
-    istiodMemory?: Metric[];
-    istiodCpu?: Metric[];
-    duration: DurationInSeconds;
-    istiodResourceThreholds?: IstiodResourceThresholds;
+  pilotLatency?: Metric[];
+  istiodMemory?: Metric[];
+  istiodCpu?: Metric[];
+  duration: DurationInSeconds;
+  istiodResourceThresholds?: IstiodResourceThresholds;
 };
 
 function showMetrics(metrics: Metric[] | undefined): boolean {
-    // show metrics if metrics exists and some values at least are not zero
-    if (metrics && metrics.length > 0 && metrics[0].datapoints.length > 0 && metrics[0].datapoints.some(dp => Number(dp[1]) !== 0)) {
-        return true;
-    }
-
-    return false;
+  // show metrics if metrics exists and some values at least are not zero
+  if (
+    metrics &&
+    metrics.length > 0 &&
+    metrics[0].datapoints.length > 0 &&
+    metrics[0].datapoints.some(dp => Number(dp[1]) !== 0)
+  ) {
+    return true;
+  }
+
+  return false;
 }
 
 class OverviewCardControlPlaneNamespace extends React.Component<ControlPlaneProps, {}> {
-    render() {
-        let memorySeries: VCLine<RichDataPoint>[] = [];
-        let cpuSeries: VCLine<RichDataPoint>[] = [];
-        let memoryThresholds: VCLine<RichDataPoint>[] = [];
-        let cpuThresholds: VCLine<RichDataPoint>[] = [];
-
-        if (showMetrics(this.props.istiodMemory)) {
-            if (this.props.istiodMemory && this.props.istiodMemory?.length > 0) {
-                const data = toVCLine(this.props.istiodMemory[0].datapoints, 'Mb', PFColors.Green400);
-
-                if (this.props.istiodResourceThreholds?.memory) {
-                    const datapoint0: Datapoint = [this.props.istiodMemory[0].datapoints[0][0], this.props.istiodMemory[0].datapoints[0][1]];
-                    datapoint0[1] = this.props.istiodResourceThreholds?.memory;
-                    const datapointn: Datapoint = [this.props.istiodMemory[0].datapoints[this.props.istiodMemory[0].datapoints.length - 1][0], this.props.istiodMemory[0].datapoints[this.props.istiodMemory[0].datapoints.length - 1][0]];
-                    datapointn[1] = this.props.istiodResourceThreholds?.memory;
-                    const dataThre = toVCLine([datapoint0, datapointn], 'Mb (Threshold)', PFColors.Green300);
-                    memoryThresholds.push(dataThre);
-                }
-
-                memorySeries.push(data);
-
-            }
+  render() {
+    let memorySeries: VCLine<RichDataPoint>[] = [];
+    let cpuSeries: VCLine<RichDataPoint>[] = [];
+    let memoryThresholds: VCLine<RichDataPoint>[] = [];
+    let cpuThresholds: VCLine<RichDataPoint>[] = [];
+
+    if (showMetrics(this.props.istiodMemory)) {
+      if (this.props.istiodMemory && this.props.istiodMemory?.length > 0) {
+        const data = toVCLine(this.props.istiodMemory[0].datapoints, 'Mb', PFColors.Green400);
+
+        if (this.props.istiodResourceThresholds?.memory) {
+          const datapoint0: Datapoint = [
+            this.props.istiodMemory[0].datapoints[0][0],
+            this.props.istiodMemory[0].datapoints[0][1]
+          ];
+          datapoint0[1] = this.props.istiodResourceThresholds?.memory;
+          const datapointn: Datapoint = [
+            this.props.istiodMemory[0].datapoints[this.props.istiodMemory[0].datapoints.length - 1][0],
+            this.props.istiodMemory[0].datapoints[this.props.istiodMemory[0].datapoints.length - 1][0]
+          ];
+          datapointn[1] = this.props.istiodResourceThresholds?.memory;
+          const dataThre = toVCLine([datapoint0, datapointn], 'Mb (Threshold)', PFColors.Green300);
+          memoryThresholds.push(dataThre);
         }
 
-        if (showMetrics(this.props.istiodCpu)) {
-            if (this.props.istiodCpu && this.props.istiodCpu?.length > 0) {
-                const data = toVCLine(this.props.istiodCpu[0].datapoints, 'cores', PFColors.Green400);
-
-                if (this.props.istiodResourceThreholds?.cpu) {
-                    const datapoint0: Datapoint = [this.props.istiodCpu[0].datapoints[0][0], this.props.istiodCpu[0].datapoints[0][1]];
-                    datapoint0[1] = this.props.istiodResourceThreholds?.cpu;
-                    const datapointn: Datapoint = [this.props.istiodCpu[0].datapoints[this.props.istiodCpu[0].datapoints.length - 1][0], this.props.istiodCpu[0].datapoints[this.props.istiodCpu[0].datapoints.length - 1][0]];
-                    datapointn[1] = this.props.istiodResourceThreholds?.cpu;
-                    const dataThre = toVCLine([datapoint0, datapointn], 'cores', PFColors.Green300);
-                    cpuThresholds.push(dataThre);
-                }
-
-                cpuSeries.push(data);
+        memorySeries.push(data);
+      }
+    }
 
-            }
+    if (showMetrics(this.props.istiodCpu)) {
+      if (this.props.istiodCpu && this.props.istiodCpu?.length > 0) {
+        const data = toVCLine(this.props.istiodCpu[0].datapoints, 'cores', PFColors.Green400);
+
+        if (this.props.istiodResourceThresholds?.cpu) {
+          const datapoint0: Datapoint = [
+            this.props.istiodCpu[0].datapoints[0][0],
+            this.props.istiodCpu[0].datapoints[0][1]
+          ];
+          datapoint0[1] = this.props.istiodResourceThresholds?.cpu;
+          const datapointn: Datapoint = [
+            this.props.istiodCpu[0].datapoints[this.props.istiodCpu[0].datapoints.length - 1][0],
+            this.props.istiodCpu[0].datapoints[this.props.istiodCpu[0].datapoints.length - 1][0]
+          ];
+          datapointn[1] = this.props.istiodResourceThresholds?.cpu;
+          const dataThre = toVCLine([datapoint0, datapointn], 'cores', PFColors.Green300);
+          cpuThresholds.push(dataThre);
         }
 
-        return (
-            <div style={{ textAlign: 'center' }}>
-                <div>
-                    <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Control plane metrics</div>
-                </div>
-                <div
-                    style={{
-                        width: '100%',
-                        verticalAlign: 'top'
-                    }}
-                >
-                    <Card isPlain>
-                        <CardBody>
-                            {showMetrics(this.props.istiodMemory) &&
-                                <Grid style={{ marginBottom: 20 }} hasGutter>
-                                    <GridItem md={2}>
-                                        <Flex
-                                            className="pf-u-h-100-on-md"
-                                            direction={{ md: 'column' }}
-                                            spaceItems={{ md: 'spaceItemsNone' }}
-                                            justifyContent={{ md: 'justifyContentCenter' }}
-                                            style={{ textAlign: "right", paddingRight: 30 }}
-                                        >
-                                            <FlexItem>
-                                                <b>Memory</b>
-                                            </FlexItem>
-                                            <FlexItem>
-                                                {getName(this.props.duration).toLowerCase()}
-                                                <Tooltip
-                                                    position={TooltipPosition.right}
-                                                    content={<div style={{ textAlign: 'left' }}>This values represents the memory of the istiod process</div>}
-                                                >
-                                                    <KialiIcon.Info className={infoStyle} />
-                                                </Tooltip>
-                                            </FlexItem>
-                                        </Flex>
-                                    </GridItem>
-                                    <GridItem md={10}>
-                                        <SparklineChart
-                                            ariaTitle='Memory'
-                                            name={'memory'}
-                                            height={65}
-                                            showLegend={false}
-                                            showYAxis={true}
-                                            padding={{ top: 10, left: 40, right: 10, bottom: 0 }}
-                                            tooltipFormat={dp => `${(dp.x as Date).toLocaleTimeString()}\n${dp.y.toFixed(2)} ${dp.name}`}
-                                            series={memorySeries}
-                                            labelName="mb"
-                                            thresholds={memoryThresholds}
-                                        />
-                                    </GridItem>
-                                </Grid>
-                            }
-                            {showMetrics(this.props.istiodCpu) &&
-                                <Grid hasGutter>
-                                    <GridItem md={2}>
-                                        <Flex
-                                            className="pf-u-h-100-on-md"
-                                            direction={{ md: 'column' }}
-                                            spaceItems={{ md: 'spaceItemsNone' }}
-                                            justifyContent={{ md: 'justifyContentCenter' }}
-                                            style={{ textAlign: "right", paddingRight: 30 }}
-                                        >
-                                            <FlexItem>
-                                                <b>CPU</b>
-                                            </FlexItem>
-                                            <FlexItem>
-                                                {getName(this.props.duration).toLowerCase()}
-                                                <Tooltip
-                                                    position={TooltipPosition.right}
-                                                    content={<div style={{ textAlign: 'left' }}>This values represents cpu of the istiod process</div>}
-                                                >
-                                                    <KialiIcon.Info className={infoStyle} />
-                                                </Tooltip>
-                                            </FlexItem>
-                                        </Flex>
-                                    </GridItem>
-                                    <GridItem md={10}>
-                                        <SparklineChart
-                                            name={'cpu'}
-                                            height={65}
-                                            showLegend={false}
-                                            showYAxis={true}
-                                            showXAxisValues={true}
-                                            padding={{ top: 10, left: 40, right: 10, bottom: 0 }}
-                                            tooltipFormat={dp => `${(dp.x as Date).toLocaleTimeString()}\n${dp.y.toFixed(2)} ${dp.name}`}
-                                            series={cpuSeries}
-                                            labelName="cores"
-                                            thresholds={cpuThresholds}
-                                        />
-                                    </GridItem>
-                                </Grid>
-                            }
-                        </CardBody>
-                    </Card>
-                </div>
-            </div>
-        );
+        cpuSeries.push(data);
+      }
     }
+
+    return (
+      <div style={{ textAlign: 'center' }}>
+        <div>
+          <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}>Control plane metrics</div>
+        </div>
+        <div
+          style={{
+            width: '100%',
+            verticalAlign: 'top'
+          }}
+        >
+          <Card isPlain>
+            <CardBody>
+              {showMetrics(this.props.istiodMemory) && (
+                <Grid style={{ marginBottom: 20 }} hasGutter>
+                  <GridItem md={2}>
+                    <Flex
+                      className="pf-u-h-100-on-md"
+                      direction={{ md: 'column' }}
+                      spaceItems={{ md: 'spaceItemsNone' }}
+                      justifyContent={{ md: 'justifyContentCenter' }}
+                      style={{ textAlign: 'right', paddingRight: 30 }}
+                    >
+                      <FlexItem>
+                        <b>Memory</b>
+                      </FlexItem>
+                      <FlexItem>
+                        {getName(this.props.duration).toLowerCase()}
+                        <Tooltip
+                          position={TooltipPosition.right}
+                          content={
+                            <div style={{ textAlign: 'left' }}>
+                              This values represents the memory of the istiod process
+                            </div>
+                          }
+                        >
+                          <KialiIcon.Info className={infoStyle} />
+                        </Tooltip>
+                      </FlexItem>
+                    </Flex>
+                  </GridItem>
+                  <GridItem md={10}>
+                    <SparklineChart
+                      ariaTitle="Memory"
+                      name={'memory'}
+                      height={65}
+                      showLegend={false}
+                      showYAxis={true}
+                      padding={{ top: 10, left: 40, right: 10, bottom: 0 }}
+                      tooltipFormat={dp =>
+                        `${(dp.x as Date).toLocaleStringWithConditionalDate()}\n${dp.y.toFixed(2)} ${dp.name}`
+                      }
+                      series={memorySeries}
+                      labelName="mb"
+                      thresholds={memoryThresholds}
+                    />
+                  </GridItem>
+                </Grid>
+              )}
+              {showMetrics(this.props.istiodCpu) && (
+                <Grid hasGutter>
+                  <GridItem md={2}>
+                    <Flex
+                      className="pf-u-h-100-on-md"
+                      direction={{ md: 'column' }}
+                      spaceItems={{ md: 'spaceItemsNone' }}
+                      justifyContent={{ md: 'justifyContentCenter' }}
+                      style={{ textAlign: 'right', paddingRight: 30 }}
+                    >
+                      <FlexItem>
+                        <b>CPU</b>
+                      </FlexItem>
+                      <FlexItem>
+                        {getName(this.props.duration).toLowerCase()}
+                        <Tooltip
+                          position={TooltipPosition.right}
+                          content={
+                            <div style={{ textAlign: 'left' }}>This values represents cpu of the istiod process</div>
+                          }
+                        >
+                          <KialiIcon.Info className={infoStyle} />
+                        </Tooltip>
+                      </FlexItem>
+                    </Flex>
+                  </GridItem>
+                  <GridItem md={10}>
+                    <SparklineChart
+                      name={'cpu'}
+                      height={65}
+                      showLegend={false}
+                      showYAxis={true}
+                      showXAxisValues={true}
+                      padding={{ top: 10, left: 40, right: 10, bottom: 0 }}
+                      tooltipFormat={dp =>
+                        `${(dp.x as Date).toLocaleStringWithConditionalDate()}\n${dp.y.toFixed(2)} ${dp.name}`
+                      }
+                      series={cpuSeries}
+                      labelName="cores"
+                      thresholds={cpuThresholds}
+                    />
+                  </GridItem>
+                </Grid>
+              )}
+            </CardBody>
+          </Card>
+        </div>
+      </div>
+    );
+  }
 }
 
 export default OverviewCardControlPlaneNamespace;
diff --git a/frontend/src/pages/Overview/OverviewCardDataPlaneNamespace.tsx b/frontend/src/pages/Overview/OverviewCardDataPlaneNamespace.tsx
index f899ed60c..0e37d8b6a 100644
--- a/frontend/src/pages/Overview/OverviewCardDataPlaneNamespace.tsx
+++ b/frontend/src/pages/Overview/OverviewCardDataPlaneNamespace.tsx
@@ -9,7 +9,7 @@ import { toVCLine } from 'utils/VictoryChartsUtils';
 
 import 'components/Charts/Charts.css';
 import { RichDataPoint, VCLine } from 'types/VictoryChartInfo';
-import { DirectionType } from "./OverviewToolbar";
+import { DirectionType } from './OverviewToolbar';
 
 type Props = {
   metrics?: Metric[];
@@ -45,18 +45,28 @@ class OverviewCardDataPlaneNamespace extends React.Component<Props, {}> {
 
     return (
       <div
-          style={{
-            width: '100%',
-            height: 130,
-            verticalAlign: 'top'
-          }}
-        >
+        style={{
+          width: '100%',
+          height: 130,
+          verticalAlign: 'top'
+        }}
+      >
         <div>
           <></>
         </div>
-        {series.length > 0 &&
+        {series.length > 0 && (
           <>
-            <div style={{ paddingTop: 10 }} data-test={'sparkline-' + this.props.direction.toLowerCase() + '-duration-' + getName(this.props.duration).toLowerCase()}>{this.props.direction + ' traffic, ' + getName(this.props.duration).toLowerCase()}</div>
+            <div
+              style={{ paddingTop: 10 }}
+              data-test={
+                'sparkline-' +
+                this.props.direction.toLowerCase() +
+                '-duration-' +
+                getName(this.props.duration).toLowerCase()
+              }
+            >
+              {this.props.direction + ' traffic, ' + getName(this.props.duration).toLowerCase()}
+            </div>
             <SparklineChart
               name={'traffics'}
               height={85}
@@ -64,17 +74,17 @@ class OverviewCardDataPlaneNamespace extends React.Component<Props, {}> {
               showYAxis={true}
               showXAxisValues={true}
               padding={{ top: 10, left: 30, right: 30, bottom: 30 }}
-              tooltipFormat={dp => `${(dp.x as Date).toLocaleTimeString()}\n${dp.y.toFixed(2)} ${dp.name}`}
+              tooltipFormat={dp =>
+                `${(dp.x as Date).toLocaleStringWithConditionalDate()}\n${dp.y.toFixed(2)} ${dp.name}`
+              }
               series={series}
               labelName="ops"
             />
           </>
-        }
-        {series.length === 0 &&
-          <div style={{ paddingTop: '40px' }}>
-            No {this.props.direction.toLowerCase()} traffic
-          </div>
-        }
+        )}
+        {series.length === 0 && (
+          <div style={{ paddingTop: '40px' }}>No {this.props.direction.toLowerCase()} traffic</div>
+        )}
       </div>
     );
   }
diff --git a/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx b/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx
index 6314231ba..923080dd1 100644
--- a/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx
+++ b/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx
@@ -10,34 +10,35 @@ import { IstiodResourceThresholds } from 'types/IstioStatus';
 type Props = {
   name: string;
   duration: DurationInSeconds;
-  direction: DirectionType
+  direction: DirectionType;
   metrics?: Metric[];
   errorMetrics?: Metric[];
   controlPlaneMetrics?: ControlPlaneMetricsMap;
-  istiodResourceThreholds?: IstiodResourceThresholds;
+  istiodResourceThresholds?: IstiodResourceThresholds;
+  istioAPIEnabled?: boolean;
 };
 
 class OverviewCardSparklineCharts extends React.Component<Props> {
   render() {
     return (
       <>
-        {this.props.name !== serverConfig.istioNamespace &&
+        {(this.props.name !== serverConfig.istioNamespace || !this.props.istioAPIEnabled) && (
           <OverviewCardDataPlaneNamespace
             metrics={this.props.metrics}
             errorMetrics={this.props.errorMetrics}
             duration={this.props.duration}
             direction={this.props.direction}
           />
-        }
-        {this.props.name === serverConfig.istioNamespace &&
+        )}
+        {this.props.name === serverConfig.istioNamespace && this.props.istioAPIEnabled && (
           <OverviewCardControlPlaneNamespace
             pilotLatency={this.props.controlPlaneMetrics?.istiod_proxy_time}
             istiodMemory={this.props.controlPlaneMetrics?.istiod_mem}
             istiodCpu={this.props.controlPlaneMetrics?.istiod_cpu}
             duration={this.props.duration}
-            istiodResourceThreholds={this.props.istiodResourceThreholds}
+            istiodResourceThresholds={this.props.istiodResourceThresholds}
           />
-        }
+        )}
       </>
     );
   }
diff --git a/frontend/src/pages/Overview/OverviewNamespaceActions.tsx b/frontend/src/pages/Overview/OverviewNamespaceActions.tsx
index d8c22399f..4cbe886e1 100644
--- a/frontend/src/pages/Overview/OverviewNamespaceActions.tsx
+++ b/frontend/src/pages/Overview/OverviewNamespaceActions.tsx
@@ -121,7 +121,7 @@ export class OverviewNamespaceActions extends React.Component<Props, State> {
         isPlain={true}
         isOpen={this.state.isKebabOpen}
         position={'right'}
-        style={{width: 22}}
+        style={{ width: 22 }}
       />
     );
   }
diff --git a/frontend/src/pages/Overview/OverviewPage.tsx b/frontend/src/pages/Overview/OverviewPage.tsx
index 7af8d442d..5221a02d3 100644
--- a/frontend/src/pages/Overview/OverviewPage.tsx
+++ b/frontend/src/pages/Overview/OverviewPage.tsx
@@ -10,6 +10,7 @@ import {
   EmptyStateVariant,
   Grid,
   GridItem,
+  Label,
   Title,
   TitleSizes,
   Tooltip,
@@ -32,7 +33,12 @@ import {
 } from '../../types/Health';
 import { SortField } from '../../types/SortFilters';
 import { PromisesRegistry } from '../../utils/CancelablePromises';
-import OverviewToolbarContainer, { OverviewDisplayMode, OverviewToolbar, OverviewType, DirectionType } from './OverviewToolbar';
+import OverviewToolbarContainer, {
+  OverviewDisplayMode,
+  OverviewToolbar,
+  OverviewType,
+  DirectionType
+} from './OverviewToolbar';
 import NamespaceInfo, { NamespaceStatus } from './NamespaceInfo';
 import NamespaceMTLSStatusContainer from '../../components/MTls/NamespaceMTLSStatus';
 import { RenderComponentScroll } from '../../components/Nav/Page';
@@ -65,8 +71,8 @@ import { MessageType } from '../../types/MessageCenter';
 import { CanaryUpgradeStatus, OutboundTrafficPolicy, ValidationStatus } from '../../types/IstioObjects';
 import { GrafanaInfo, ISTIO_DASHBOARDS } from '../../types/GrafanaInfo';
 import { ExternalLink } from '../../types/Dashboards';
-import { isParentKiosk, kioskOverviewAction } from "../../components/Kiosk/KioskActions";
-import ValidationSummaryLinkContainer from "../../components/Link/ValidationSummaryLink";
+import { isParentKiosk, kioskOverviewAction } from '../../components/Kiosk/KioskActions';
+import ValidationSummaryLinkContainer from '../../components/Link/ValidationSummaryLink';
 import _ from 'lodash';
 import ControlPlaneBadge from './ControlPlaneBadge';
 import OverviewStatusContainer from './OverviewStatus';
@@ -146,18 +152,20 @@ type State = {
   nsTarget: string;
   opTarget: string;
   grafanaLinks: ExternalLink[];
-  istiodResourceThreholds: IstiodResourceThresholds;
+  istiodResourceThresholds: IstiodResourceThresholds;
   outboundPolicyMode: OutboundTrafficPolicy;
   canaryUpgradeStatus?: CanaryUpgradeStatus;
 };
 
 type ReduxProps = {
   duration: DurationInSeconds;
+  isMaistra: boolean;
+  istioAPIEnabled: boolean;
   kiosk: string;
   meshStatus: string;
+  minTLS: string;
   navCollapse: boolean;
   refreshInterval: IntervalInMilliseconds;
-  minTLS: string;
 };
 
 type OverviewProps = ReduxProps & {};
@@ -181,7 +189,7 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
       nsTarget: '',
       opTarget: '',
       grafanaLinks: [],
-      istiodResourceThreholds: { memory: 0, cpu: 0 },
+      istiodResourceThresholds: { memory: 0, cpu: 0 },
       outboundPolicyMode: {},
       canaryUpgradeStatus: undefined
     };
@@ -261,7 +269,7 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
               showTrafficPoliciesModal: prevState.showTrafficPoliciesModal,
               kind: prevState.kind,
               nsTarget: prevState.nsTarget,
-              opTarget: prevState.opTarget,
+              opTarget: prevState.opTarget
             };
           },
           () => {
@@ -406,7 +414,7 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
       step: rateParams.step,
       rateInterval: rateParams.rateInterval,
       direction: direction,
-      reporter: direction === 'inbound' ? 'destination' : 'source',
+      reporter: direction === 'inbound' ? 'destination' : 'source'
     };
 
     return Promise.all(
@@ -419,7 +427,7 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
               istiod_proxy_time: rs.data.pilot_proxy_convergence_time,
               istiod_cpu: rs.data.process_cpu_seconds_total,
               istiod_mem: rs.data.process_virtual_memory_bytes
-            }
+            };
           }
           return nsInfo;
         });
@@ -483,16 +491,11 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
       nss.push(ns.name);
     });
 
-    return Promise.all(
-      [
-        API.getConfigValidations(nss),
-        API.getAllIstioConfigs(nss, [], false, '', '')
-      ]
-    )
+    return Promise.all([API.getConfigValidations(nss), API.getAllIstioConfigs(nss, [], false, '', '')])
       .then(results => {
         chunk.forEach(nsInfo => {
-          nsInfo.validations = results[0].data[nsInfo.name]
-          nsInfo.istioConfig = results[1].data[nsInfo.name]
+          nsInfo.validations = results[0].data[nsInfo.name];
+          nsInfo.istioConfig = results[1].data[nsInfo.name];
         });
       })
       .catch(err => this.handleAxiosError('Could not fetch validations status', err));
@@ -501,12 +504,12 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
   fetchOutboundTrafficPolicyMode() {
     API.getOutboundTrafficPolicyMode()
       .then(response => {
-        this.setState({ outboundPolicyMode: { mode: response.data.mode } })
+        this.setState({ outboundPolicyMode: { mode: response.data.mode } });
       })
       .catch(error => {
         AlertUtils.addError('Error fetching Mesh OutboundTrafficPolicy.Mode.', error, 'default', MessageType.ERROR);
       });
-  };
+  }
 
   fetchCanariesStatus() {
     API.getCanaryUpgradeStatus()
@@ -518,22 +521,22 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
             migratedNamespaces: response.data.migratedNamespaces,
             pendingNamespaces: response.data.pendingNamespaces
           }
-        })
+        });
       })
       .catch(error => {
         AlertUtils.addError('Error fetching canary upgrade status.', error, 'default', MessageType.ERROR);
       });
-  };
+  }
 
   fetchIstiodResourceThresholds() {
     API.getIstiodResourceThresholds()
       .then(response => {
-        this.setState({ istiodResourceThreholds: response.data })
+        this.setState({ istiodResourceThresholds: response.data });
       })
       .catch(error => {
-        AlertUtils.addError('Error fetching Istiod resource threholds.', error, 'default', MessageType.ERROR);
+        AlertUtils.addError('Error fetching Istiod resource thresholds.', error, 'default', MessageType.ERROR);
       });
-  };
+  }
 
   handleAxiosError(message: string, error: AxiosError) {
     FilterHelper.handleError(`${message}: ${API.getErrorString(error)}`);
@@ -557,10 +560,10 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
     return (
       !!ns.status &&
       ns.status.inError.length +
-      ns.status.inSuccess.length +
-      ns.status.inWarning.length +
-      ns.status.notAvailable.length ===
-      0
+        ns.status.inSuccess.length +
+        ns.status.inWarning.length +
+        ns.status.notAvailable.length ===
+        0
     );
   };
 
@@ -591,79 +594,87 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
   getNamespaceActions = (nsInfo: NamespaceInfo): OverviewNamespaceAction[] => {
     // Today actions are fixed, but soon actions may depend of the state of a namespace
     // So we keep this wrapped in a showActions function.
-    const namespaceActions: OverviewNamespaceAction[] = isParentKiosk(this.props.kiosk) ? [
-      {
-        isGroup: true,
-        isSeparator: false,
-        isDisabled: false,
-        title: 'Show',
-        children: [
+    const namespaceActions: OverviewNamespaceAction[] = isParentKiosk(this.props.kiosk)
+      ? [
           {
             isGroup: true,
             isSeparator: false,
-            title: 'Graph',
-            action: (ns: string) => kioskOverviewAction(Show.GRAPH, ns, this.props.duration, this.props.refreshInterval)
-          },
-          {
-            isGroup: true,
-            isSeparator: false,
-            title: 'Istio Config',
-            action: (ns: string) => kioskOverviewAction(Show.ISTIO_CONFIG, ns, this.props.duration, this.props.refreshInterval)
+            isDisabled: false,
+            title: 'Show',
+            children: [
+              {
+                isGroup: true,
+                isSeparator: false,
+                title: 'Graph',
+                action: (ns: string) =>
+                  kioskOverviewAction(Show.GRAPH, ns, this.props.duration, this.props.refreshInterval)
+              },
+              {
+                isGroup: true,
+                isSeparator: false,
+                title: 'Istio Config',
+                action: (ns: string) =>
+                  kioskOverviewAction(Show.ISTIO_CONFIG, ns, this.props.duration, this.props.refreshInterval)
+              }
+            ]
           }
         ]
-      }
-    ] : [
-      {
-        isGroup: true,
-        isSeparator: false,
-        isDisabled: false,
-        title: 'Show',
-        children: [
-          {
-            isGroup: true,
-            isSeparator: false,
-            title: 'Graph',
-            action: (ns: string) => this.show(Show.GRAPH, ns, this.state.type)
-          },
-          {
-            isGroup: true,
-            isSeparator: false,
-            title: 'Applications',
-            action: (ns: string) => this.show(Show.APPLICATIONS, ns, this.state.type)
-          },
-          {
-            isGroup: true,
-            isSeparator: false,
-            title: 'Workloads',
-            action: (ns: string) => this.show(Show.WORKLOADS, ns, this.state.type)
-          },
-          {
-            isGroup: true,
-            isSeparator: false,
-            title: 'Services',
-            action: (ns: string) => this.show(Show.SERVICES, ns, this.state.type)
-          },
+      : [
           {
             isGroup: true,
             isSeparator: false,
-            title: 'Istio Config',
-            action: (ns: string) => this.show(Show.ISTIO_CONFIG, ns, this.state.type)
+            isDisabled: false,
+            title: 'Show',
+            children: [
+              {
+                isGroup: true,
+                isSeparator: false,
+                title: 'Graph',
+                action: (ns: string) => this.show(Show.GRAPH, ns, this.state.type)
+              },
+              {
+                isGroup: true,
+                isSeparator: false,
+                title: 'Applications',
+                action: (ns: string) => this.show(Show.APPLICATIONS, ns, this.state.type)
+              },
+              {
+                isGroup: true,
+                isSeparator: false,
+                title: 'Workloads',
+                action: (ns: string) => this.show(Show.WORKLOADS, ns, this.state.type)
+              },
+              {
+                isGroup: true,
+                isSeparator: false,
+                title: 'Services',
+                action: (ns: string) => this.show(Show.SERVICES, ns, this.state.type)
+              },
+              {
+                isGroup: true,
+                isSeparator: false,
+                title: 'Istio Config',
+                action: (ns: string) => this.show(Show.ISTIO_CONFIG, ns, this.state.type)
+              }
+            ]
           }
-        ]
-      }
-    ];
+        ];
     // We are going to assume that if the user can create/update Istio AuthorizationPolicies in a namespace
     // then it can use the Istio Injection Actions.
     // RBAC allow more fine granularity but Kiali won't check that in detail.
 
     if (serverConfig.istioNamespace !== nsInfo.name) {
-      if (serverConfig.kialiFeatureFlags.istioInjectionAction && !serverConfig.kialiFeatureFlags.istioUpgradeAction) {
+      if (
+        !this.props.isMaistra &&
+        serverConfig.kialiFeatureFlags.istioInjectionAction &&
+        !serverConfig.kialiFeatureFlags.istioUpgradeAction
+      ) {
         namespaceActions.push({
           isGroup: false,
           isSeparator: true
         });
         const enableAction = {
-          "data-test": `enable-${nsInfo.name}-namespace-sidecar-injection`,
+          'data-test': `enable-${nsInfo.name}-namespace-sidecar-injection`,
           isGroup: false,
           isSeparator: false,
           title: 'Enable Auto Injection',
@@ -671,7 +682,7 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
             this.setState({ showTrafficPoliciesModal: true, nsTarget: ns, opTarget: 'enable', kind: 'injection' })
         };
         const disableAction = {
-          "data-test": `disable-${nsInfo.name}-namespace-sidecar-injection`,
+          'data-test': `disable-${nsInfo.name}-namespace-sidecar-injection`,
           isGroup: false,
           isSeparator: false,
           title: 'Disable Auto Injection',
@@ -679,7 +690,7 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
             this.setState({ showTrafficPoliciesModal: true, nsTarget: ns, opTarget: 'disable', kind: 'injection' })
         };
         const removeAction = {
-          "data-test": `remove-${nsInfo.name}-namespace-sidecar-injection`,
+          'data-test': `remove-${nsInfo.name}-namespace-sidecar-injection`,
           isGroup: false,
           isSeparator: false,
           title: 'Remove Auto Injection',
@@ -704,12 +715,8 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
         } else {
           namespaceActions.push(enableAction);
         }
-        namespaceActions.push({
-          isGroup: false,
-          isSeparator: true,
-          isDisabled: false
-        });
       }
+
       if (
         serverConfig.kialiFeatureFlags.istioUpgradeAction &&
         serverConfig.istioCanaryRevision.upgrade &&
@@ -741,22 +748,15 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
               nsInfo.labels[serverConfig.istioLabels.injectionLabelName] === 'enabled'))
         ) {
           namespaceActions.push(upgradeAction);
-          namespaceActions.push({
-            isGroup: false,
-            isSeparator: true
-          });
         } else if (
           nsInfo.labels &&
           nsInfo.labels[serverConfig.istioLabels.injectionLabelRev] &&
           nsInfo.labels[serverConfig.istioLabels.injectionLabelRev] === serverConfig.istioCanaryRevision.upgrade
         ) {
           namespaceActions.push(downgradeAction);
-          namespaceActions.push({
-            isGroup: false,
-            isSeparator: true
-          });
         }
       }
+
       const aps = nsInfo.istioConfig?.authorizationPolicies || [];
       const addAuthorizationAction = {
         isGroup: false,
@@ -778,9 +778,15 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
         action: (ns: string) =>
           this.setState({ opTarget: 'delete', nsTarget: ns, showTrafficPoliciesModal: true, kind: 'policy' })
       };
-      namespaceActions.push(addAuthorizationAction);
-      if (aps.length > 0) {
-        namespaceActions.push(removeAuthorizationAction);
+      if (this.props.istioAPIEnabled) {
+        namespaceActions.push({
+          isGroup: false,
+          isSeparator: true
+        });
+        namespaceActions.push(addAuthorizationAction);
+        if (aps.length > 0) {
+          namespaceActions.push(removeAuthorizationAction);
+        }
       }
     } else if (this.state.grafanaLinks.length > 0) {
       // Istio namespace will render external Grafana dashboards
@@ -817,12 +823,15 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
 
   hasCanaryUpgradeConfigured = (): boolean => {
     if (this.state.canaryUpgradeStatus) {
-      if (this.state.canaryUpgradeStatus.pendingNamespaces.length > 0 || this.state.canaryUpgradeStatus.migratedNamespaces.length > 0) {
+      if (
+        this.state.canaryUpgradeStatus.pendingNamespaces.length > 0 ||
+        this.state.canaryUpgradeStatus.migratedNamespaces.length > 0
+      ) {
         return true;
       }
     }
     return false;
-  }
+  };
 
   render() {
     const sm = this.state.displayMode === OverviewDisplayMode.COMPACT ? 3 : 6;
@@ -864,14 +873,31 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
                 {filteredNamespaces.map((ns, i) => {
                   const isLongNs = ns.name.length > NS_LONG;
                   return (
-                    <GridItem sm={(ns.name === serverConfig.istioNamespace && this.state.displayMode === OverviewDisplayMode.EXPAND) ? lg : sm}
-                      md={(ns.name === serverConfig.istioNamespace && this.state.displayMode === OverviewDisplayMode.EXPAND) ? lg : md}
+                    <GridItem
+                      sm={
+                        ns.name === serverConfig.istioNamespace &&
+                        this.state.displayMode === OverviewDisplayMode.EXPAND &&
+                        (this.props.istioAPIEnabled || this.hasCanaryUpgradeConfigured())
+                          ? lg
+                          : sm
+                      }
+                      md={
+                        ns.name === serverConfig.istioNamespace &&
+                        this.state.displayMode === OverviewDisplayMode.EXPAND &&
+                        (this.props.istioAPIEnabled || this.hasCanaryUpgradeConfigured())
+                          ? lg
+                          : md
+                      }
                       key={'CardItem_' + ns.name}
-                      style={{ margin: '0px 5px 0 5px' }}>
+                      style={{ margin: '0px 5px 0 5px' }}
+                    >
                       <Card
                         isCompact={true}
-                        className={(ns.name === serverConfig.istioNamespace) ? cardControlPlaneGridStyle : cardGridStyle}
+                        className={ns.name === serverConfig.istioNamespace ? cardControlPlaneGridStyle : cardGridStyle}
                         data-test={ns.name + '-' + OverviewDisplayMode[this.state.displayMode]}
+                        style={
+                          !this.props.istioAPIEnabled && !this.hasCanaryUpgradeConfigured() ? { height: '96%' } : {}
+                        }
                       >
                         <CardHeader>
                           <CardHeaderMain>
@@ -881,58 +907,93 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
                                 title={ns.name}
                               >
                                 {ns.name}
-                                {ns.name === serverConfig.istioNamespace &&
-                                  <ControlPlaneBadge></ControlPlaneBadge>
-                                }
-                                {ns.name !== serverConfig.istioNamespace && this.hasCanaryUpgradeConfigured() && this.state.canaryUpgradeStatus?.migratedNamespaces.includes(ns.name) &&
-                                  <ControlPlaneVersionBadge version={this.state.canaryUpgradeStatus.upgradeVersion} isCanary={true}></ControlPlaneVersionBadge>
-                                }
-                                {ns.name !== serverConfig.istioNamespace && this.hasCanaryUpgradeConfigured() && this.state.canaryUpgradeStatus?.pendingNamespaces.includes(ns.name) &&
-                                  <ControlPlaneVersionBadge version={this.state.canaryUpgradeStatus.currentVersion} isCanary={false}></ControlPlaneVersionBadge>
-                                }
+                                {ns.name === serverConfig.istioNamespace && <ControlPlaneBadge></ControlPlaneBadge>}
+                                {ns.name !== serverConfig.istioNamespace &&
+                                  this.hasCanaryUpgradeConfigured() &&
+                                  this.state.canaryUpgradeStatus?.migratedNamespaces.includes(ns.name) && (
+                                    <ControlPlaneVersionBadge
+                                      version={this.state.canaryUpgradeStatus.upgradeVersion}
+                                      isCanary={true}
+                                    ></ControlPlaneVersionBadge>
+                                  )}
+                                {ns.name !== serverConfig.istioNamespace &&
+                                  this.hasCanaryUpgradeConfigured() &&
+                                  this.state.canaryUpgradeStatus?.pendingNamespaces.includes(ns.name) && (
+                                    <ControlPlaneVersionBadge
+                                      version={this.state.canaryUpgradeStatus.currentVersion}
+                                      isCanary={false}
+                                    ></ControlPlaneVersionBadge>
+                                  )}
+                                {ns.name === serverConfig.istioNamespace && !this.props.istioAPIEnabled && (
+                                  <Label style={{ marginLeft: 10 }} color={'orange'} isCompact>
+                                    Istio API disabled
+                                  </Label>
+                                )}
                               </span>
                             </Title>
                           </CardHeaderMain>
                           <CardActions>{namespaceActions[i]}</CardActions>
                         </CardHeader>
                         <CardBody>
-                          {ns.name === serverConfig.istioNamespace && this.state.displayMode === OverviewDisplayMode.EXPAND &&
-                            <Grid>
-                              <GridItem md={3} >
-                                {this.renderLabels(ns)}
-
-                                <div style={{ textAlign: 'left' }}>
-                                  <div style={{ display: 'inline-block', width: '125px' }}>Istio config</div>
-                                  {ns.tlsStatus && (
-                                    <span>
-                                      <NamespaceMTLSStatusContainer status={ns.tlsStatus.status} />
-                                    </span>
+                          {ns.name === serverConfig.istioNamespace &&
+                            this.state.displayMode === OverviewDisplayMode.EXPAND && (
+                              <Grid>
+                                <GridItem md={this.props.istioAPIEnabled || this.hasCanaryUpgradeConfigured() ? 3 : 6}>
+                                  {this.renderLabels(ns)}
+
+                                  <div style={{ textAlign: 'left' }}>
+                                    <div style={{ display: 'inline-block', width: '125px' }}>Istio config</div>
+                                    {ns.tlsStatus && (
+                                      <span>
+                                        <NamespaceMTLSStatusContainer status={ns.tlsStatus.status} />
+                                      </span>
+                                    )}
+                                    {this.props.istioAPIEnabled ? this.renderIstioConfigStatus(ns) : 'N/A'}
+                                  </div>
+                                  {ns.status && (
+                                    <NamespaceStatuses
+                                      key={ns.name}
+                                      name={ns.name}
+                                      status={ns.status}
+                                      type={this.state.type}
+                                    />
+                                  )}
+                                  {this.state.displayMode === OverviewDisplayMode.EXPAND && (
+                                    <ControlPlaneNamespaceStatus
+                                      outboundTrafficPolicy={this.state.outboundPolicyMode}
+                                      namespace={ns}
+                                    ></ControlPlaneNamespaceStatus>
+                                  )}
+                                  {this.state.displayMode === OverviewDisplayMode.EXPAND && (
+                                    <TLSInfo
+                                      certificatesInformationIndicators={
+                                        serverConfig.kialiFeatureFlags.certificatesInformationIndicators.enabled
+                                      }
+                                      version={this.props.minTLS}
+                                    ></TLSInfo>
                                   )}
-                                  {this.renderIstioConfigStatus(ns)}
-                                </div>
-                                {ns.status && <NamespaceStatuses key={ns.name} name={ns.name} status={ns.status} type={this.state.type} />}
-                                {this.state.displayMode === OverviewDisplayMode.EXPAND && <ControlPlaneNamespaceStatus outboundTrafficPolicy={this.state.outboundPolicyMode} namespace={ns}></ControlPlaneNamespaceStatus>}
-                                {this.state.displayMode === OverviewDisplayMode.EXPAND && <TLSInfo certificatesInformationIndicators={serverConfig.kialiFeatureFlags.certificatesInformationIndicators.enabled} version={this.props.minTLS}></TLSInfo> }
-                              </GridItem>
-                              {ns.name === serverConfig.istioNamespace &&
-                                <GridItem md={9}>
-                                  <Grid>
-                                    {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() &&
-                                      <GridItem md={4}>
-                                        <CanaryUpgradeProgress canaryUpgradeStatus={this.state.canaryUpgradeStatus} />
-                                      </GridItem>
-                                    }
-                                    <GridItem md={(this.hasCanaryUpgradeConfigured()) ? 8 : 12}>
-                                      {this.renderCharts(ns)}
-                                    </GridItem>
-
-                                  </Grid>
-
                                 </GridItem>
-                              }
-                            </Grid>
-                          }
-                          {((ns.name !== serverConfig.istioNamespace && this.state.displayMode === OverviewDisplayMode.EXPAND) || this.state.displayMode === OverviewDisplayMode.COMPACT) &&
+                                {ns.name === serverConfig.istioNamespace && (
+                                  <GridItem md={9}>
+                                    <Grid>
+                                      {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && (
+                                        <GridItem md={this.props.istioAPIEnabled ? 4 : 9}>
+                                          <CanaryUpgradeProgress canaryUpgradeStatus={this.state.canaryUpgradeStatus} />
+                                        </GridItem>
+                                      )}
+                                      {this.props.istioAPIEnabled === true && (
+                                        <GridItem md={this.hasCanaryUpgradeConfigured() ? 8 : 12}>
+                                          {this.renderCharts(ns)}
+                                        </GridItem>
+                                      )}
+                                    </Grid>
+                                  </GridItem>
+                                )}
+                              </Grid>
+                            )}
+                          {((ns.name !== serverConfig.istioNamespace &&
+                            this.state.displayMode === OverviewDisplayMode.EXPAND) ||
+                            this.state.displayMode === OverviewDisplayMode.COMPACT) && (
                             <div>
                               {this.renderLabels(ns)}
 
@@ -943,14 +1004,12 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
                                     <NamespaceMTLSStatusContainer status={ns.tlsStatus.status} />
                                   </span>
                                 )}
-                                {this.renderIstioConfigStatus(ns)}
+                                {this.props.istioAPIEnabled ? this.renderIstioConfigStatus(ns) : 'N/A'}
                               </div>
                               {this.renderStatus(ns)}
                               {this.state.displayMode === OverviewDisplayMode.EXPAND && this.renderCharts(ns)}
                             </div>
-                          }
-
-
+                          )}
                         </CardBody>
                       </Card>
                     </GridItem>
@@ -1032,7 +1091,8 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
           metrics={ns.metrics}
           errorMetrics={ns.errorMetrics}
           controlPlaneMetrics={ns.controlPlaneMetrics}
-          istiodResourceThreholds={this.state.istiodResourceThreholds}
+          istiodResourceThresholds={this.state.istiodResourceThresholds}
+          istioAPIEnabled={this.props.istioAPIEnabled}
         />
       );
     }
@@ -1081,7 +1141,14 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
     } else {
       text = nbItems + switchType(this.state.type, ' applications', ' services', ' workloads');
     }
-    const mainLink = <div style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }} data-test={'overview-type-' + this.state.type}>{text}</div>;
+    const mainLink = (
+      <div
+        style={{ display: 'inline-block', width: '125px', whiteSpace: 'nowrap' }}
+        data-test={'overview-type-' + this.state.type}
+      >
+        {text}
+      </div>
+    );
     if (nbItems === ns.status?.notAvailable.length) {
       return (
         <div style={{ textAlign: 'left' }}>
@@ -1143,14 +1210,15 @@ export class OverviewPage extends React.Component<OverviewProps, State> {
   }
 }
 
-
 const mapStateToProps = (state: KialiAppState): ReduxProps => ({
   duration: durationSelector(state),
+  isMaistra: state.statusState.istioEnvironment.isMaistra,
+  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled,
   kiosk: state.globalState.kiosk,
   meshStatus: meshWideMTLSStatusSelector(state),
-  navCollapse: state.userSettings.interface.navCollapse,
-  refreshInterval: refreshIntervalSelector(state),
   minTLS: minTLSVersionSelector(state),
+  navCollapse: state.userSettings.interface.navCollapse,
+  refreshInterval: refreshIntervalSelector(state)
 });
 
 const OverviewPageContainer = connect(mapStateToProps)(OverviewPage);
diff --git a/frontend/src/pages/Overview/OverviewStatus.tsx b/frontend/src/pages/Overview/OverviewStatus.tsx
index bcff5fbd4..113a0cbeb 100644
--- a/frontend/src/pages/Overview/OverviewStatus.tsx
+++ b/frontend/src/pages/Overview/OverviewStatus.tsx
@@ -9,17 +9,17 @@ import { FilterSelected } from '../../components/Filters/StatefulFilters';
 import { createIcon } from '../../components/Health/Helper';
 
 import '../../components/Health/Health.css';
-import {KialiAppState} from "../../store/Store";
-import {connect} from "react-redux";
-import {isParentKiosk, kioskGraphAction} from "../../components/Kiosk/KioskActions";
-import {durationSelector, refreshIntervalSelector} from "../../store/Selectors";
-import {DurationInSeconds, IntervalInMilliseconds} from "../../types/Common";
+import { KialiAppState } from '../../store/Store';
+import { connect } from 'react-redux';
+import { isParentKiosk, kioskGraphAction } from '../../components/Kiosk/KioskActions';
+import { durationSelector, refreshIntervalSelector } from '../../store/Selectors';
+import { DurationInSeconds, IntervalInMilliseconds } from '../../types/Common';
 
 type ReduxProps = {
   duration: DurationInSeconds;
   kiosk: string;
   refreshInterval: IntervalInMilliseconds;
-}
+};
 
 type Props = ReduxProps & {
   id: string;
@@ -44,11 +44,17 @@ class OverviewStatus extends React.Component<Props, {}> {
     // Kiosk actions are used when the kiosk specifies a parent,
     // otherwise the kiosk=true will keep the links inside Kiali
     if (isParentKiosk(this.props.kiosk)) {
-      kioskGraphAction(this.props.namespace, this.props.status.name, this.props.duration, this.props.refreshInterval, this.props.targetPage)
+      kioskGraphAction(
+        this.props.namespace,
+        this.props.status.name,
+        this.props.duration,
+        this.props.refreshInterval,
+        this.props.targetPage
+      );
     } else {
-      this.setFilters()
+      this.setFilters();
     }
-  }
+  };
 
   render() {
     const length = this.props.items.length;
@@ -91,8 +97,8 @@ class OverviewStatus extends React.Component<Props, {}> {
 const mapStateToProps = (state: KialiAppState) => ({
   duration: durationSelector(state),
   kiosk: state.globalState.kiosk,
-  refreshInterval: refreshIntervalSelector(state),
+  refreshInterval: refreshIntervalSelector(state)
 });
 
-const OverviewStatusContainer = connect(mapStateToProps)(OverviewStatus)
+const OverviewStatusContainer = connect(mapStateToProps)(OverviewStatus);
 export default OverviewStatusContainer;
diff --git a/frontend/src/pages/Overview/OverviewToolbar.tsx b/frontend/src/pages/Overview/OverviewToolbar.tsx
index 7bcf5ed1d..bba071a9e 100644
--- a/frontend/src/pages/Overview/OverviewToolbar.tsx
+++ b/frontend/src/pages/Overview/OverviewToolbar.tsx
@@ -18,8 +18,8 @@ import * as Filters from './Filters';
 import { style } from 'typestyle';
 import { PFColors } from '../../components/Pf/PfColors';
 import TimeDurationContainer from '../../components/Time/TimeDurationComponent';
-import { KialiDispatch } from "../../types/Redux";
-import RefreshNotifier from "../../components/Refresh/RefreshNotifier";
+import { KialiDispatch } from '../../types/Redux';
+import RefreshNotifier from '../../components/Refresh/RefreshNotifier';
 
 type ReduxProps = {
   duration: DurationInSeconds;
@@ -220,11 +220,7 @@ export class OverviewToolbar extends React.Component<Props, State> {
     const timeToolbar = (
       <div className={timeToolbarStyle}>
         <RefreshNotifier onTick={this.props.onRefresh} />
-        <TimeDurationContainer
-          key="overview-time-range"
-          id="overview-time-range"
-          disabled={false}
-        />
+        <TimeDurationContainer key="overview-time-range" id="overview-time-range" disabled={false} />
       </div>
     );
     const actionsToolbar = (
diff --git a/frontend/src/pages/Overview/Sorts.ts b/frontend/src/pages/Overview/Sorts.ts
index 06af0b046..ed423620e 100644
--- a/frontend/src/pages/Overview/Sorts.ts
+++ b/frontend/src/pages/Overview/Sorts.ts
@@ -92,8 +92,9 @@ export const sortFields: SortField<NamespaceInfo>[] = [
 ];
 
 export const sortFunc = (allNamespaces: NamespaceInfo[], sortField: SortField<NamespaceInfo>, isAscending: boolean) => {
-  var sortedNamespaces = allNamespaces.filter(ns => ns.name !== serverConfig.istioNamespace)
-    .sort(isAscending ? sortField.compare : (a, b) => sortField.compare(b, a))
+  var sortedNamespaces = allNamespaces
+    .filter(ns => ns.name !== serverConfig.istioNamespace)
+    .sort(isAscending ? sortField.compare : (a, b) => sortField.compare(b, a));
 
   return allNamespaces.filter(ns => ns.name === serverConfig.istioNamespace).concat(sortedNamespaces);
 };
diff --git a/frontend/src/pages/Overview/__tests__/OverviewPage.test.tsx b/frontend/src/pages/Overview/__tests__/OverviewPage.test.tsx
index be20d02a0..8019b1a31 100644
--- a/frontend/src/pages/Overview/__tests__/OverviewPage.test.tsx
+++ b/frontend/src/pages/Overview/__tests__/OverviewPage.test.tsx
@@ -101,7 +101,16 @@ describe('Overview page', () => {
 
   it('renders initial layout', () => {
     const wrapper = shallow(
-      <OverviewPage meshStatus={MTLSStatuses.NOT_ENABLED} navCollapse={false} duration={600} refreshInterval={10000} kiosk={''} minTLS={''}/>
+      <OverviewPage
+        meshStatus={MTLSStatuses.NOT_ENABLED}
+        navCollapse={false}
+        duration={600}
+        refreshInterval={10000}
+        kiosk={''}
+        minTLS={''}
+        istioAPIEnabled={false}
+        isMaistra={false}
+      />
     );
     expect(shallowToJson(wrapper)).toMatchSnapshot();
   });
diff --git a/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts b/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts
index e3297dc27..2c194cb8b 100644
--- a/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts
+++ b/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts
@@ -87,7 +87,7 @@ describe('Overview Page ', () => {
       'electronic-shop',
       'alpha',
       'beta',
-      'fraud-detection',      
+      'fraud-detection',
       'travel-agency',
       'travel-control',
       'travel-portal'
@@ -100,7 +100,7 @@ describe('Overview Page ', () => {
       'istio-system',
       'travel-portal',
       'travel-control',
-      'travel-agency',      
+      'travel-agency',
       'fraud-detection',
       'beta',
       'alpha',
diff --git a/frontend/src/pages/ServiceDetails/ServiceDescription.tsx b/frontend/src/pages/ServiceDetails/ServiceDescription.tsx
index 0cb1f10bf..3cfa9c637 100644
--- a/frontend/src/pages/ServiceDetails/ServiceDescription.tsx
+++ b/frontend/src/pages/ServiceDetails/ServiceDescription.tsx
@@ -11,7 +11,7 @@ import { TextOrLink } from '../../components/TextOrLink';
 import { KialiIcon } from '../../config/KialiIcon';
 import { HealthIndicator } from '../../components/Health/HealthIndicator';
 import { PFBadge, PFBadges } from '../../components/Pf/PfBadges';
-import DetailDescriptionContainer from "../../components/Details/DetailDescription";
+import DetailDescriptionContainer from '../../components/Details/DetailDescription';
 
 interface ServiceInfoDescriptionProps {
   namespace: string;
diff --git a/frontend/src/pages/ServiceDetails/ServiceDetailsPage.tsx b/frontend/src/pages/ServiceDetails/ServiceDetailsPage.tsx
index 4a589978c..0257574fd 100644
--- a/frontend/src/pages/ServiceDetails/ServiceDetailsPage.tsx
+++ b/frontend/src/pages/ServiceDetails/ServiceDetailsPage.tsx
@@ -16,21 +16,21 @@ import TrafficDetails from 'components/TrafficList/TrafficDetails';
 import * as API from '../../services/Api';
 import * as AlertUtils from '../../utils/AlertUtils';
 import { PromisesRegistry } from '../../utils/CancelablePromises';
-import {getServiceWizardLabel, ServiceDetailsInfo} from '../../types/ServiceInfo';
+import { getServiceWizardLabel, ServiceDetailsInfo } from '../../types/ServiceInfo';
 import {
   Gateway,
   K8sGateway,
   getGatewaysAsList,
   PeerAuthentication,
   Validations,
-  getK8sGatewaysAsList,
+  getK8sGatewaysAsList
 } from '../../types/IstioObjects';
 import ServiceWizardDropdown from '../../components/IstioWizards/ServiceWizardDropdown';
 import TimeControl from '../../components/Time/TimeControl';
-import RenderHeaderContainer from "../../components/Nav/Page/RenderHeader";
-import {ErrorMsg} from "../../types/ErrorMsg";
-import ErrorSection from "../../components/ErrorSection/ErrorSection";
-import connectRefresh from "../../components/Refresh/connectRefresh";
+import RenderHeaderContainer from '../../components/Nav/Page/RenderHeader';
+import { ErrorMsg } from '../../types/ErrorMsg';
+import ErrorSection from '../../components/ErrorSection/ErrorSection';
+import connectRefresh from '../../components/Refresh/connectRefresh';
 
 type ServiceDetailsState = {
   currentTab: string;
@@ -121,8 +121,11 @@ class ServiceDetails extends React.Component<ServiceDetailsProps, ServiceDetails
       })
       .catch(error => {
         AlertUtils.addError('Could not fetch Service Details.', error);
-        const msg : ErrorMsg = {title: 'No Service is selected', description: this.props.match.params.service +" is not found in the mesh"};
-        this.setState({error: msg});
+        const msg: ErrorMsg = {
+          title: 'No Service is selected',
+          description: this.props.match.params.service + ' is not found in the mesh'
+        };
+        this.setState({ error: msg });
       });
 
     API.getIstioConfig(this.props.match.params.namespace, ['peerauthentications'], false, '', '')
@@ -230,9 +233,7 @@ class ServiceDetails extends React.Component<ServiceDetailsProps, ServiceDetails
           rightToolbar={<TimeControl customDuration={useCustomTime} />}
           actionsToolbar={actionsToolbar}
         />
-        {this.state.error && (
-          <ErrorSection error={this.state.error} />
-        )}
+        {this.state.error && <ErrorSection error={this.state.error} />}
         {!this.state.error && (
           <ParameterizedTabs
             id="basic-tabs"
@@ -248,15 +249,15 @@ class ServiceDetails extends React.Component<ServiceDetailsProps, ServiceDetails
           >
             {this.renderTabs()}
           </ParameterizedTabs>
-          )}
+        )}
       </>
     );
   }
 }
 
 const mapStateToProps = (state: KialiAppState) => ({
-  jaegerInfo: state.jaegerState.info,
+  jaegerInfo: state.jaegerState.info
 });
 
-const ServiceDetailsPageContainer = connectRefresh( connect(mapStateToProps)(ServiceDetails));
+const ServiceDetailsPageContainer = connectRefresh(connect(mapStateToProps)(ServiceDetails));
 export default ServiceDetailsPageContainer;
diff --git a/frontend/src/pages/ServiceDetails/ServiceInfo.tsx b/frontend/src/pages/ServiceDetails/ServiceInfo.tsx
index e5da23e64..c7c0a0f06 100644
--- a/frontend/src/pages/ServiceDetails/ServiceInfo.tsx
+++ b/frontend/src/pages/ServiceDetails/ServiceInfo.tsx
@@ -8,7 +8,9 @@ import { ServiceDetailsInfo } from '../../types/ServiceInfo';
 import {
   DestinationRuleC,
   Gateway,
-  getGatewaysAsList, getK8sGatewaysAsList, K8sGateway,
+  getGatewaysAsList,
+  getK8sGatewaysAsList,
+  K8sGateway,
   ObjectValidation,
   PeerAuthentication,
   Validations
@@ -23,22 +25,23 @@ import {
   gwToIstioItems,
   seToIstioItems,
   k8sHTTPRouteToIstioItems,
-  validationKey, k8sGwToIstioItems
+  validationKey,
+  k8sGwToIstioItems
 } from '../../types/IstioConfigList';
-import { canCreate, canUpdate } from "../../types/Permissions";
+import { canCreate, canUpdate } from '../../types/Permissions';
 import { KialiAppState } from '../../store/Store';
 import { durationSelector, meshWideMTLSEnabledSelector } from '../../store/Selectors';
 import ServiceNetwork from './ServiceNetwork';
 import { GraphEdgeTapEvent } from '../../components/CytoscapeGraph/CytoscapeGraph';
 import history, { URLParam } from '../../app/History';
-import MiniGraphCardContainer from "../../components/CytoscapeGraph/MiniGraphCard";
-import IstioConfigCard from "../../components/IstioConfigCard/IstioConfigCard";
-import ServiceWizard from "../../components/IstioWizards/ServiceWizard";
-import ConfirmDeleteTrafficRoutingModal from "../../components/IstioWizards/ConfirmDeleteTrafficRoutingModal";
-import { WizardAction, WizardMode } from "../../components/IstioWizards/WizardActions";
-import { deleteServiceTrafficRouting } from "../../services/Api";
-import * as AlertUtils from "../../utils/AlertUtils";
-import { triggerRefresh } from "../../hooks/refresh";
+import MiniGraphCardContainer from '../../components/CytoscapeGraph/MiniGraphCard';
+import IstioConfigCard from '../../components/IstioConfigCard/IstioConfigCard';
+import ServiceWizard from '../../components/IstioWizards/ServiceWizard';
+import ConfirmDeleteTrafficRoutingModal from '../../components/IstioWizards/ConfirmDeleteTrafficRoutingModal';
+import { WizardAction, WizardMode } from '../../components/IstioWizards/WizardActions';
+import { deleteServiceTrafficRouting } from '../../services/Api';
+import * as AlertUtils from '../../utils/AlertUtils';
+import { triggerRefresh } from '../../hooks/refresh';
 
 interface Props extends ServiceId {
   duration: DurationInSeconds;
@@ -48,6 +51,7 @@ interface Props extends ServiceId {
   k8sGateways: K8sGateway[];
   peerAuthentications: PeerAuthentication[];
   validations: Validations;
+  istioAPIEnabled: boolean;
 }
 
 type ServiceInfoState = {
@@ -126,11 +130,11 @@ class ServiceInfo extends React.Component<Props, ServiceInfoState> {
     if (changed) {
       triggerRefresh();
     }
-  }
+  };
 
   private handleConfirmDeleteServiceTrafficRouting = () => {
     this.setState({
-      showConfirmDeleteTrafficRouting:false
+      showConfirmDeleteTrafficRouting: false
     });
 
     deleteServiceTrafficRouting(this.props.serviceDetails!)
@@ -150,7 +154,7 @@ class ServiceInfo extends React.Component<Props, ServiceInfoState> {
     this.setState({
       showWizard: true,
       wizardType: action,
-      updateMode: mode === "update",
+      updateMode: mode === 'update'
     });
   };
 
@@ -172,18 +176,21 @@ class ServiceInfo extends React.Component<Props, ServiceInfoState> {
     const k8sGwIstioConfigItems =
       this.props?.k8sGateways && this.props.serviceDetails?.k8sHTTPRoutes
         ? k8sGwToIstioItems(
-          this.props?.k8sGateways,
-          this.props.serviceDetails.k8sHTTPRoutes
-        )
+            this.props?.k8sGateways,
+            this.props.serviceDetails.k8sHTTPRoutes,
+            this.props.serviceDetails.validations
+          )
         : [];
     const seIstioConfigItems = this.props.serviceDetails?.serviceEntries
       ? seToIstioItems(this.props.serviceDetails.serviceEntries, this.props.serviceDetails.validations)
       : [];
     const k8sHTTPRouteIstioConfigItems = this.props.serviceDetails?.k8sHTTPRoutes
-      ? k8sHTTPRouteToIstioItems(this.props.serviceDetails.k8sHTTPRoutes)
+      ? k8sHTTPRouteToIstioItems(this.props.serviceDetails.k8sHTTPRoutes, this.props.serviceDetails.validations)
       : [];
     const istioConfigItems = seIstioConfigItems.concat(
-      gwIstioConfigItems.concat(k8sGwIstioConfigItems.concat(vsIstioConfigItems.concat(drIstioConfigItems.concat(k8sHTTPRouteIstioConfigItems))))
+      gwIstioConfigItems.concat(
+        k8sGwIstioConfigItems.concat(vsIstioConfigItems.concat(drIstioConfigItems.concat(k8sHTTPRouteIstioConfigItems)))
+      )
     );
 
     // RenderComponentScroll handles height to provide an inner scroll combined with tabs
@@ -234,7 +241,10 @@ class ServiceInfo extends React.Component<Props, ServiceInfoState> {
           serviceName={this.props.serviceDetails?.service?.name || ''}
           workloads={this.props.serviceDetails?.workloads || []}
           subServices={this.props.serviceDetails?.subServices || []}
-          createOrUpdate={canCreate(this.props.serviceDetails?.istioPermissions) || canUpdate(this.props.serviceDetails?.istioPermissions)}
+          createOrUpdate={
+            canCreate(this.props.serviceDetails?.istioPermissions) ||
+            canUpdate(this.props.serviceDetails?.istioPermissions)
+          }
           virtualServices={this.props.serviceDetails?.virtualServices || []}
           destinationRules={this.props.serviceDetails?.destinationRules || []}
           gateways={getGatewaysAsList(this.props.gateways)}
@@ -243,6 +253,7 @@ class ServiceInfo extends React.Component<Props, ServiceInfoState> {
           peerAuthentications={this.props.peerAuthentications}
           tlsStatus={this.props.serviceDetails?.namespaceMTLS}
           onClose={this.handleWizardClose}
+          istioAPIEnabled={this.props.istioAPIEnabled}
         />
         {this.state.showConfirmDeleteTrafficRouting && (
           <ConfirmDeleteTrafficRoutingModal
@@ -250,7 +261,7 @@ class ServiceInfo extends React.Component<Props, ServiceInfoState> {
             virtualServices={this.props.serviceDetails!.virtualServices}
             k8sHTTPRoutes={this.props.serviceDetails!.k8sHTTPRoutes}
             isOpen={true}
-            onCancel={() => this.setState({showConfirmDeleteTrafficRouting: false})}
+            onCancel={() => this.setState({ showConfirmDeleteTrafficRouting: false })}
             onConfirm={this.handleConfirmDeleteServiceTrafficRouting}
           />
         )}
@@ -261,7 +272,8 @@ class ServiceInfo extends React.Component<Props, ServiceInfoState> {
 
 const mapStateToProps = (state: KialiAppState) => ({
   duration: durationSelector(state),
-  mtlsEnabled: meshWideMTLSEnabledSelector(state)
+  mtlsEnabled: meshWideMTLSEnabledSelector(state),
+  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled
 });
 
 const ServiceInfoContainer = connect(mapStateToProps)(ServiceInfo);
diff --git a/frontend/src/pages/ServiceList/ServiceListPage.tsx b/frontend/src/pages/ServiceList/ServiceListPage.tsx
index 804ff4b66..03608cf98 100644
--- a/frontend/src/pages/ServiceList/ServiceListPage.tsx
+++ b/frontend/src/pages/ServiceList/ServiceListPage.tsx
@@ -22,7 +22,7 @@ import TimeDurationContainer from '../../components/Time/TimeDurationComponent';
 import { sortIstioReferences } from '../AppList/FiltersAndSorts';
 import { validationKey } from '../../types/IstioConfigList';
 import { ServiceHealth } from '../../types/Health';
-import RefreshNotifier from "../../components/Refresh/RefreshNotifier";
+import RefreshNotifier from '../../components/Refresh/RefreshNotifier';
 
 type ServiceListPageState = FilterComponent.State<ServiceListItem>;
 
@@ -111,7 +111,7 @@ class ServiceListPageComponent extends FilterComponent.Component<
         ports: service.ports || {},
         istioReferences: sortIstioReferences(service.istioReferences, true),
         kialiWizard: service.kialiWizard,
-        serviceRegistry: service.serviceRegistry,
+        serviceRegistry: service.serviceRegistry
       }));
     }
     return [];
@@ -159,11 +159,7 @@ class ServiceListPageComponent extends FilterComponent.Component<
         <div style={{ backgroundColor: '#fff' }}>
           <DefaultSecondaryMasthead
             rightToolbar={
-              <TimeDurationContainer
-                key={'DurationDropdown'}
-                id="service-list-duration-dropdown"
-                disabled={false}
-              />
+              <TimeDurationContainer key={'DurationDropdown'} id="service-list-duration-dropdown" disabled={false} />
             }
           />
         </div>
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadDescription.tsx b/frontend/src/pages/WorkloadDetails/WorkloadDescription.tsx
index 73819eee6..06de064eb 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadDescription.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadDescription.tsx
@@ -15,8 +15,8 @@ import { PFBadge, PFBadges } from '../../components/Pf/PfBadges';
 import MissingLabel from '../../components/MissingLabel/MissingLabel';
 import MissingAuthPolicy from 'components/MissingAuthPolicy/MissingAuthPolicy';
 import { hasMissingAuthPolicy } from 'utils/IstioConfigUtils';
-import DetailDescriptionContainer from "../../components/Details/DetailDescription";
-import { isGateway } from "../../helpers/LabelFilterHelper";
+import DetailDescriptionContainer from '../../components/Details/DetailDescription';
+import { isGateway } from '../../helpers/LabelFilterHelper';
 
 type WorkloadDescriptionProps = {
   workload?: Workload;
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadDetailsPage.tsx b/frontend/src/pages/WorkloadDetails/WorkloadDetailsPage.tsx
index 4545d0c68..760410111 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadDetailsPage.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadDetailsPage.tsx
@@ -23,11 +23,10 @@ import TimeControl from '../../components/Time/TimeControl';
 import EnvoyDetailsContainer from 'components/Envoy/EnvoyDetails';
 import { StatusState } from '../../types/StatusState';
 import { WorkloadHealth } from 'types/Health';
-import RenderHeaderContainer from "../../components/Nav/Page/RenderHeader";
-import ErrorSection from "../../components/ErrorSection/ErrorSection";
-import {ErrorMsg} from "../../types/ErrorMsg";
-import connectRefresh from "../../components/Refresh/connectRefresh";
-
+import RenderHeaderContainer from '../../components/Nav/Page/RenderHeader';
+import ErrorSection from '../../components/ErrorSection/ErrorSection';
+import { ErrorMsg } from '../../types/ErrorMsg';
+import connectRefresh from '../../components/Refresh/connectRefresh';
 
 type WorkloadDetailsState = {
   workload?: Workload;
@@ -42,9 +41,10 @@ type ReduxProps = {
   statusState: StatusState;
 };
 
-type WorkloadDetailsPageProps = ReduxProps & RouteComponentProps<WorkloadId> & {
-  lastRefreshAt: TimeInMilliseconds;
-};
+type WorkloadDetailsPageProps = ReduxProps &
+  RouteComponentProps<WorkloadId> & {
+    lastRefreshAt: TimeInMilliseconds;
+  };
 
 export const tabName = 'tab';
 export const defaultTab = 'info';
@@ -80,16 +80,15 @@ class WorkloadDetails extends React.Component<WorkloadDetailsPageProps, Workload
     ) {
       if (currentTab === 'info' || currentTab === 'logs' || currentTab === 'envoy') {
         this.fetchWorkload().then(() => {
-            if (currentTab !== this.state.currentTab) {
-              this.setState({ currentTab: currentTab });
-            }
+          if (currentTab !== this.state.currentTab) {
+            this.setState({ currentTab: currentTab });
+          }
         });
       } else {
         if (currentTab !== this.state.currentTab) {
           this.setState({ currentTab: currentTab });
         }
       }
-
     }
   }
 
@@ -113,8 +112,11 @@ class WorkloadDetails extends React.Component<WorkloadDetailsPageProps, Workload
       })
       .catch(error => {
         AlertUtils.addError('Could not fetch Workload.', error);
-        const msg : ErrorMsg = {title: 'No Workload is selected', description: this.props.match.params.workload +" is not found in the mesh"};
-        this.setState({error: msg});
+        const msg: ErrorMsg = {
+          title: 'No Workload is selected',
+          description: this.props.match.params.workload + ' is not found in the mesh'
+        };
+        this.setState({ error: msg });
       });
   };
 
@@ -149,7 +151,7 @@ class WorkloadDetails extends React.Component<WorkloadDetailsPageProps, Workload
 
     if (!serverConfig.kialiFeatureFlags.disabledFeatures?.includes('logs-tab')) {
       const logTab = (
-        <Tab title="Logs" eventKey={2} key={'Logs'} data-test={"workload-details-logs-tab"}>
+        <Tab title="Logs" eventKey={2} key={'Logs'} data-test={'workload-details-logs-tab'}>
           {hasPods ? (
             <WorkloadPodLogs
               lastRefreshAt={this.props.lastRefreshAt}
@@ -217,7 +219,8 @@ class WorkloadDetails extends React.Component<WorkloadDetailsPageProps, Workload
             <EnvoyDetailsContainer
               lastRefreshAt={this.props.lastRefreshAt}
               namespace={this.props.match.params.namespace}
-              workload={this.state.workload} />
+              workload={this.state.workload}
+            />
           )}
         </Tab>
       );
@@ -291,7 +294,6 @@ class WorkloadDetails extends React.Component<WorkloadDetailsPageProps, Workload
   }
 
   render() {
-
     // set default to true: all dynamic tabs (unlisted below) are for runtimes dashboards, which uses custom time
     let useCustomTime = true;
     switch (this.state.currentTab) {
@@ -322,9 +324,7 @@ class WorkloadDetails extends React.Component<WorkloadDetailsPageProps, Workload
           rightToolbar={<TimeControl customDuration={useCustomTime} />}
           actionsToolbar={actionsToolbar}
         />
-        {this.state.error && (
-          <ErrorSection error={this.state.error} />
-          )}
+        {this.state.error && <ErrorSection error={this.state.error} />}
         {this.state.workload && (
           <ParameterizedTabs
             id="basic-tabs"
@@ -349,7 +349,7 @@ class WorkloadDetails extends React.Component<WorkloadDetailsPageProps, Workload
 const mapStateToProps = (state: KialiAppState) => ({
   duration: durationSelector(state),
   jaegerInfo: state.jaegerState.info,
-  statusState: state.statusState,
+  statusState: state.statusState
 });
 
 const WorkloadDetailsContainer = connectRefresh(connect(mapStateToProps)(WorkloadDetails));
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx b/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx
index d9639c3b5..79609d1ca 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx
@@ -19,8 +19,8 @@ import { meshWideMTLSEnabledSelector } from '../../store/Selectors';
 import WorkloadPods from './WorkloadPods';
 import { GraphEdgeTapEvent } from '../../components/CytoscapeGraph/CytoscapeGraph';
 import history, { URLParam } from '../../app/History';
-import MiniGraphCardContainer from "../../components/CytoscapeGraph/MiniGraphCard";
-import IstioConfigCard from "../../components/IstioConfigCard/IstioConfigCard";
+import MiniGraphCardContainer from '../../components/CytoscapeGraph/MiniGraphCard';
+import IstioConfigCard from '../../components/IstioConfigCard/IstioConfigCard';
 
 type WorkloadInfoProps = {
   duration: DurationInSeconds;
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx b/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
index b5b609464..cab57889d 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
@@ -48,10 +48,11 @@ import moment from 'moment';
 import { formatDuration } from 'utils/tracing/TracingHelper';
 import { infoStyle } from 'styles/DropdownStyles';
 import { isValid } from 'utils/Common';
-import {isKiosk} from "../../components/Kiosk/KioskActions";
-import { KioskElement } from "../../components/Kiosk/KioskElement";
-import { TimeDurationModal } from "../../components/Time/TimeDurationModal";
-import TimeDurationIndicatorContainer from "../../components/Time/TimeDurationIndicatorComponent";
+import { isKiosk } from '../../components/Kiosk/KioskActions';
+import { KioskElement } from '../../components/Kiosk/KioskElement';
+import { TimeDurationModal } from '../../components/Time/TimeDurationModal';
+import TimeDurationIndicatorContainer from '../../components/Time/TimeDurationIndicatorComponent';
+import { serverConfig } from '../../config';
 
 const appContainerColors = [PFColors.White, PFColors.LightGreen400, PFColors.Purple100, PFColors.LightBlue400];
 const proxyContainerColor = PFColors.Gold400;
@@ -176,11 +177,13 @@ const toolbarInputStyle = style({
 const logsBackground = (enabled: boolean) => ({ backgroundColor: enabled ? PFColors.Black1000 : 'gray' });
 const logsHeight = (showToolbar: boolean, fullscreen: boolean, kiosk: string, showMaxLinesWarning: boolean) => {
   const toolbarHeight = showToolbar ? '0px' : '49px';
-  const maxLinesWarningHeight = showMaxLinesWarning ? '27px' : '0px'
+  const maxLinesWarningHeight = showMaxLinesWarning ? '27px' : '0px';
   return {
     height: fullscreen
       ? `calc(100vh - 130px + ${toolbarHeight} - ${maxLinesWarningHeight})`
-      : `calc(var(--kiali-details-pages-tab-content-height) - ${!isKiosk(kiosk) ? '155px' : '0px'} + ${toolbarHeight} - ${maxLinesWarningHeight})`
+      : `calc(var(--kiali-details-pages-tab-content-height) - ${
+          !isKiosk(kiosk) ? '155px' : '0px'
+        } + ${toolbarHeight} - ${maxLinesWarningHeight})`
   };
 };
 
@@ -406,7 +409,8 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
           customDuration={true}
           isOpen={this.state.isTimeOptionsOpen}
           onConfirm={this.toggleTimeOptionsVisibility}
-          onCancel={this.toggleTimeOptionsVisibility} />
+          onCancel={this.toggleTimeOptionsVisibility}
+        />
       </>
     );
   }
@@ -459,18 +463,23 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
   };
 
   private toggleTimeOptionsVisibility = () => {
-    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }) );
-  }
+    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }));
+  };
 
-  private renderLogLine = ({index, style}: {index: number, style: Object}) => {
-    let e = this.filteredEntries(this.state.entries, this.state.showLogValue, this.state.hideLogValue, this.state.useRegex)[index];
+  private renderLogLine = ({ index, style }: { index: number; style: Object }) => {
+    let e = this.filteredEntries(
+      this.state.entries,
+      this.state.showLogValue,
+      this.state.hideLogValue,
+      this.state.useRegex
+    )[index];
     if (e.span) {
       return (
         <div key={`s-${index}`} style={{ height: '22px', lineHeight: '22px', paddingLeft: '10px', ...style }}>
           {this.state.showTimestamps && (
             <span key={`al-s-${index}`} style={{ color: spanColor, fontSize: '12px', marginRight: '5px' }}>
-                            {e.timestamp}
-                          </span>
+              {e.timestamp}
+            </span>
           )}
           <Tooltip
             key={`al-tt-${index}`}
@@ -520,8 +529,8 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
       <div key={`al-${index}`} style={{ height: '22px', lineHeight: '22px', paddingLeft: '10px', ...style }}>
         {this.state.showTimestamps && (
           <span key={`al-s-${index}`} style={{ color: le.color!, fontSize: '12px', marginRight: '5px' }}>
-                          {formatDate(le.timestamp)}
-                        </span>
+            {formatDate(le.timestamp)}
+          </span>
         )}
         <Tooltip
           key={`al-tt-${index}`}
@@ -565,6 +574,7 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
           onClick={() => {
             this.setLogLevel(LogLevel[level]);
           }}
+          isDisabled={serverConfig.deployment.viewOnlyMode}
         >
           {level}
         </DropdownItem>
@@ -583,7 +593,7 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
                 the log level to 'off' disables the proxy loggers but does <b>not</b> disable access logging. To hide
                 all proxy logging from the logs view, including access logs, un-check the proxy container. <br />
                 <br />
-                This option is disabled for pods with no proxy container.
+                This option is disabled for pods with no proxy container, or in view-only mode.
               </div>
             </div>
           }
@@ -609,7 +619,9 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
       </DropdownGroup>
     ];
 
-    const logEntries = this.state.entries ? this.filteredEntries(this.state.entries, this.state.showLogValue, this.state.hideLogValue, this.state.useRegex) : [];
+    const logEntries = this.state.entries
+      ? this.filteredEntries(this.state.entries, this.state.showLogValue, this.state.hideLogValue, this.state.useRegex)
+      : [];
     return (
       <div key="logsDiv" id="logsDiv" className={logsDiv}>
         <Toolbar style={{ padding: '5px 0' }}>
@@ -648,9 +660,16 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
             </ToolbarItem>
           </ToolbarGroup>
         </Toolbar>
-        { this.state.linesTruncatedContainers.length > 0 && (
-          <div style={{marginBottom: '5px'}}>
-            <Alert variant="danger" isInline={true} isPlain={true} title={`Max lines exceeded for containers: ${this.state.linesTruncatedContainers.join(', ')}. Increase maxLines for more lines, or decrease time period.`} />
+        {this.state.linesTruncatedContainers.length > 0 && (
+          <div style={{ marginBottom: '5px' }}>
+            <Alert
+              variant="danger"
+              isInline={true}
+              isPlain={true}
+              title={`Max lines exceeded for containers: ${this.state.linesTruncatedContainers.join(
+                ', '
+              )}. Increase maxLines for more lines, or decrease time period.`}
+            />
           </div>
         )}
         <div
@@ -662,12 +681,17 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
           // (to max) and when we try to assign scrollTop to scrollHeight (above),it stays at 0
           // and we fail to set the scroll correctly. So, don't change this!
           style={{
-            ...logsHeight(this.state.showToolbar, this.state.fullscreen, this.props.kiosk, this.state.linesTruncatedContainers.length > 0),
+            ...logsHeight(
+              this.state.showToolbar,
+              this.state.fullscreen,
+              this.props.kiosk,
+              this.state.linesTruncatedContainers.length > 0
+            ),
             ...logsBackground(this.hasEntries(this.state.entries))
           }}
         >
           <AutoSizer>
-            {({height, width}) => (
+            {({ height, width }) => (
               <List
                 ref={this.logsRef}
                 rowHeight={22}
@@ -676,7 +700,9 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
                 height={height}
                 width={width}
                 scrollToIndex={logEntries.length - 1}
-                noRowsRenderer={() => (<div style={{ paddingTop: '10px', paddingLeft: '10px' }}>{NoLogsFoundMessage}</div>)}
+                noRowsRenderer={() => (
+                  <div style={{ paddingTop: '10px', paddingLeft: '10px' }}>{NoLogsFoundMessage}</div>
+                )}
                 containerStyle={{ overflow: 'initial !important' }}
               />
             )}
@@ -792,7 +818,7 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
       if (useRegex) {
         try {
           const regexp = RegExp(showValue);
-          filteredEntries = filteredEntries.filter(e => (!e.logEntry || regexp.test(e.logEntry.message)));
+          filteredEntries = filteredEntries.filter(e => !e.logEntry || regexp.test(e.logEntry.message));
           if (!!this.state.showError) {
             this.setState({ showError: undefined });
           }
@@ -800,7 +826,7 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
           this.setState({ showError: `Show: ${e.message}` });
         }
       } else {
-        filteredEntries = filteredEntries.filter(e => (!e.logEntry || e.logEntry.message.includes(showValue)));
+        filteredEntries = filteredEntries.filter(e => !e.logEntry || e.logEntry.message.includes(showValue));
       }
     }
 
@@ -808,7 +834,7 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
       if (useRegex) {
         try {
           const regexp = RegExp(hideValue);
-          filteredEntries = filteredEntries.filter(e => (!e.logEntry || !regexp.test(e.logEntry.message)));
+          filteredEntries = filteredEntries.filter(e => !e.logEntry || !regexp.test(e.logEntry.message));
           if (!!this.state.hideError) {
             this.setState({ hideError: undefined });
           }
@@ -816,7 +842,7 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
           this.setState({ hideError: `Hide: ${e.message}` });
         }
       } else {
-        filteredEntries = filteredEntries.filter(e => (!e.logEntry || !e.logEntry.message.includes(hideValue)));
+        filteredEntries = filteredEntries.filter(e => !e.logEntry || !e.logEntry.message.includes(hideValue));
       }
     }
 
@@ -946,11 +972,9 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
         if (showSpans) {
           const spans = showSpans ? (responses[0].data as Span[]) : ([] as Span[]);
           entries = spans.map(span => {
-             let startTimeU = Math.floor(span.startTime / 1000);
+            let startTimeU = Math.floor(span.startTime / 1000);
             return {
-              timestamp: moment(startTimeU)
-                .utc()
-                .format('YYYY-MM-DD HH:mm:ss.SSS'),
+              timestamp: moment(startTimeU).utc().format('YYYY-MM-DD HH:mm:ss.SSS'),
               timestampUnix: startTimeU,
               span: span
             } as Entry;
@@ -1024,7 +1048,6 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
   };
 
   private entryToString = (entry: Entry): string => {
-
     if (entry.logEntry) {
       const le = entry.logEntry;
       return this.state.showTimestamps ? `${formatDate(entry.timestamp)} ${le.message}` : le.message;
@@ -1038,17 +1061,15 @@ export class WorkloadPodLogs extends React.Component<WorkloadPodLogsProps, Workl
 }
 
 const formatDate = (timestamp: string): string => {
+  let entryTimestamp = moment(timestamp).format('YYYY-MM-DD HH:mm:ss.SSS');
 
-  let entryTimestamp = moment(timestamp)
-      .format('YYYY-MM-DD HH:mm:ss.SSS')
-
-  return entryTimestamp
+  return entryTimestamp;
 };
 
 const mapStateToProps = (state: KialiAppState) => {
   return {
     kiosk: state.globalState.kiosk,
-    timeRange: timeRangeSelector(state),
+    timeRange: timeRangeSelector(state)
   };
 };
 
diff --git a/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx b/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx
index ca5ec1855..1bf828867 100644
--- a/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx
+++ b/frontend/src/pages/WorkloadDetails/__tests__/WorkloadPodLogs.test.tsx
@@ -1,10 +1,10 @@
 import * as React from 'react';
-import { Provider } from "react-redux";
+import { Provider } from 'react-redux';
 import { mount, shallow } from 'enzyme';
 import screenfull, { Screenfull } from 'screenfull';
 import { WorkloadPodLogs } from '../WorkloadPodLogs';
 import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core';
-import { store } from "../../../store/ConfigStore";
+import { store } from '../../../store/ConfigStore';
 
 const defaultProps = () => ({
   kiosk: '',
@@ -59,10 +59,7 @@ describe('WorkloadPodLogs', () => {
         <WorkloadPodLogs {...defaultProps()} />
       </Provider>
     );
-    wrapper
-      .find(KebabToggle)
-      .find('button')
-      .simulate('click');
+    wrapper.find(KebabToggle).find('button').simulate('click');
     expect(
       wrapper
         .find(DropdownItem)
@@ -79,10 +76,7 @@ describe('WorkloadPodLogs', () => {
         <WorkloadPodLogs {...props} />
       </Provider>
     );
-    wrapper
-      .find(KebabToggle)
-      .find('button')
-      .simulate('click');
+    wrapper.find(KebabToggle).find('button').simulate('click');
     expect(
       wrapper
         .find(DropdownItem)
@@ -100,10 +94,7 @@ describe('WorkloadPodLogs', () => {
         <WorkloadPodLogs {...defaultProps()} />
       </Provider>
     );
-    wrapper
-      .find(KebabToggle)
-      .find('button')
-      .simulate('click');
+    wrapper.find(KebabToggle).find('button').simulate('click');
     wrapper
       .find(DropdownItem)
       .findWhere(n => n.key() === 'setLogLevelDebug')
diff --git a/frontend/src/pages/WorkloadList/WorkloadListPage.tsx b/frontend/src/pages/WorkloadList/WorkloadListPage.tsx
index fb6f47aca..d6fc8b67e 100644
--- a/frontend/src/pages/WorkloadList/WorkloadListPage.tsx
+++ b/frontend/src/pages/WorkloadList/WorkloadListPage.tsx
@@ -21,7 +21,7 @@ import TimeDurationContainer from '../../components/Time/TimeDurationComponent';
 import { sortIstioReferences } from '../AppList/FiltersAndSorts';
 import { hasMissingAuthPolicy } from 'utils/IstioConfigUtils';
 import { WorkloadHealth } from '../../types/Health';
-import RefreshNotifier from "../../components/Refresh/RefreshNotifier";
+import RefreshNotifier from '../../components/Refresh/RefreshNotifier';
 
 type WorkloadListPageState = FilterComponent.State<WorkloadListItem>;
 
@@ -147,11 +147,7 @@ class WorkloadListPageComponent extends FilterComponent.Component<
         <div style={{ backgroundColor: '#fff' }}>
           <DefaultSecondaryMasthead
             rightToolbar={
-              <TimeDurationContainer
-                key={'DurationDropdown'}
-                id="workload-list-duration-dropdown"
-                disabled={false}
-              />
+              <TimeDurationContainer key={'DurationDropdown'} id="workload-list-duration-dropdown" disabled={false} />
             }
           />
         </div>
diff --git a/frontend/src/polyfills.ts b/frontend/src/polyfills.ts
deleted file mode 100644
index 191a37d48..000000000
--- a/frontend/src/polyfills.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-// Polyfills window.customElements; currently required for Firefox ESR
-// It can be removed once is supported
-import '@webcomponents/custom-elements';
diff --git a/frontend/src/reducers/HelpDropdownState.ts b/frontend/src/reducers/HelpDropdownState.ts
index 3ef9f7ad7..cc38d7f03 100644
--- a/frontend/src/reducers/HelpDropdownState.ts
+++ b/frontend/src/reducers/HelpDropdownState.ts
@@ -8,7 +8,8 @@ export const INITIAL_STATUS_STATE: StatusState = {
   externalServices: [],
   warningMessages: [],
   istioEnvironment: {
-    isMaistra: false
+    isMaistra: false,
+    istioAPIEnabled: true
   }
 };
 
@@ -19,7 +20,8 @@ const HelpDropdownState = (state: StatusState = INITIAL_STATUS_STATE, action: Ki
         ...INITIAL_STATUS_STATE,
         status: action.payload.status,
         externalServices: action.payload.externalServices,
-        warningMessages: action.payload.warningMessages
+        warningMessages: action.payload.warningMessages,
+        istioEnvironment: action.payload.istioEnvironment
       };
     default:
       return state;
diff --git a/frontend/src/reducers/MeshTlsState.ts b/frontend/src/reducers/MeshTlsState.ts
index 2abc5bad2..0191745a5 100644
--- a/frontend/src/reducers/MeshTlsState.ts
+++ b/frontend/src/reducers/MeshTlsState.ts
@@ -6,7 +6,7 @@ import { MeshTlsActions } from '../actions/MeshTlsActions';
 export const INITIAL_MESH_TLS_STATE: TLSStatus = {
   status: '',
   autoMTLSEnabled: false,
-  minTLS: '',
+  minTLS: ''
 };
 
 // This Reducer allows changes to the 'graphDataState' portion of Redux Store
diff --git a/frontend/src/reducers/MessageCenter.ts b/frontend/src/reducers/MessageCenter.ts
index 96a18fc67..27850ec15 100644
--- a/frontend/src/reducers/MessageCenter.ts
+++ b/frontend/src/reducers/MessageCenter.ts
@@ -101,11 +101,9 @@ const Messages = (
 
           if (existingMessage) {
             // it is in the list already
-            firstTriggered = existingMessage.firstTriggered
-              ? existingMessage.firstTriggered
-              : existingMessage.created;
+            firstTriggered = existingMessage.firstTriggered ? existingMessage.firstTriggered : existingMessage.created;
 
-            count += existingMessage.count
+            count += existingMessage.count;
           }
 
           newMessage = createMessage(
diff --git a/frontend/src/services/Api.ts b/frontend/src/services/Api.ts
index a552c810c..b053052aa 100644
--- a/frontend/src/services/Api.ts
+++ b/frontend/src/services/Api.ts
@@ -663,4 +663,4 @@ export const getCrippledFeatures = (): Promise<Response<KialiCrippledFeatures>>
 
 export const getCanaryUpgradeStatus = () => {
   return newRequest<CanaryUpgradeStatus>(HTTP_VERBS.GET, urls.canaryUpgradeStatus(), {}, {});
-};
\ No newline at end of file
+};
diff --git a/frontend/src/services/Login.ts b/frontend/src/services/Login.ts
index 1e3347657..930088f77 100644
--- a/frontend/src/services/Login.ts
+++ b/frontend/src/services/Login.ts
@@ -4,7 +4,7 @@ import moment from 'moment';
 import { LoginSession, KialiAppState } from '../store/Store';
 import { AuthStrategy, AuthResult, AuthConfig } from '../types/Auth';
 import { TimeInMilliseconds } from '../types/Common';
-import { KialiDispatch } from "../types/Redux";
+import { KialiDispatch } from '../types/Redux';
 import authenticationConfig from '../config/AuthenticationConfig';
 
 export interface LoginResult {
diff --git a/frontend/src/store/Selectors/GraphData.ts b/frontend/src/store/Selectors/GraphData.ts
index 738b8fff5..7cbf1e100 100644
--- a/frontend/src/store/Selectors/GraphData.ts
+++ b/frontend/src/store/Selectors/GraphData.ts
@@ -158,22 +158,37 @@ export const decorateGraphData = (graphData: GraphElements, duration: number): D
           if (Array.isArray(decoratedNode.data.healthData)) {
             decoratedNode.data.healthStatus = NA.name;
           } else if (decoratedNode.data.healthData.workloadStatus) {
-            decoratedNode.data.health = WorkloadHealth.fromJson(decoratedNode.data.namespace, decoratedNode.data.workload, decoratedNode.data.healthData, {
-              rateInterval: duration,
-              hasSidecar: true
-            })
+            decoratedNode.data.health = WorkloadHealth.fromJson(
+              decoratedNode.data.namespace,
+              decoratedNode.data.workload,
+              decoratedNode.data.healthData,
+              {
+                rateInterval: duration,
+                hasSidecar: true
+              }
+            );
             decoratedNode.data.healthStatus = decoratedNode.data.health.getGlobalStatus().name;
           } else if (decoratedNode.data.healthData.workloadStatuses) {
-            decoratedNode.data.health = AppHealth.fromJson(decoratedNode.data.namespace, decoratedNode.data.app, decoratedNode.data.healthData, {
-              rateInterval: duration,
-              hasSidecar: true
-            })
+            decoratedNode.data.health = AppHealth.fromJson(
+              decoratedNode.data.namespace,
+              decoratedNode.data.app,
+              decoratedNode.data.healthData,
+              {
+                rateInterval: duration,
+                hasSidecar: true
+              }
+            );
             decoratedNode.data.healthStatus = decoratedNode.data.health.getGlobalStatus().name;
           } else {
-            decoratedNode.data.health = ServiceHealth.fromJson(decoratedNode.data.namespace, decoratedNode.data.service, decoratedNode.data.healthData, {
-              rateInterval: duration,
-              hasSidecar: true
-            });
+            decoratedNode.data.health = ServiceHealth.fromJson(
+              decoratedNode.data.namespace,
+              decoratedNode.data.service,
+              decoratedNode.data.healthData,
+              {
+                rateInterval: duration,
+                hasSidecar: true
+              }
+            );
             decoratedNode.data.healthStatus = decoratedNode.data.health.getGlobalStatus().name;
           }
         }
diff --git a/frontend/src/styles/DropdownStyles.ts b/frontend/src/styles/DropdownStyles.ts
index d009ff4b5..091171f21 100644
--- a/frontend/src/styles/DropdownStyles.ts
+++ b/frontend/src/styles/DropdownStyles.ts
@@ -8,7 +8,7 @@ export const containerStyle = style({
 
 // this emulates Select component .pf-c-select__menu
 export const menuStyle = style({
-  fontSize: 'var(--kiali-global--font-size)',
+  fontSize: 'var(--kiali-global--font-size)'
 });
 
 // this emulates Select component .pf-c-select__menu but w/o cursor manipulation
@@ -31,10 +31,10 @@ const itemStyle: NestedCSSProperties = {
   padding: '6px 16px',
   $nest: {
     '& > div > input.pf-c-radio__input': {
-      margin: '1px 0 0 0',
+      margin: '1px 0 0 0'
     },
     '& > div > input.pf-c-check__input': {
-      margin: '1px 0 0 0',
+      margin: '1px 0 0 0'
     }
   }
 };
diff --git a/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts b/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
index 191bf77ba..3c0bbb5e3 100644
--- a/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
+++ b/frontend/src/types/ErrorRate/__testData__/ErrorRateConfig.ts
@@ -67,8 +67,11 @@ export const generateRequestHealth = (
 };
 
 export const serverRateConfig = {
+  accessibleNamespaces: [],
+  authStrategy: '',
   clusters: {},
   gatewayAPIEnabled: false,
+  logLevel: '',
   kialiFeatureFlags: {
     certificatesInformationIndicators: {
       enabled: true
diff --git a/frontend/src/types/Graph.ts b/frontend/src/types/Graph.ts
index 382a49baa..b86f8cc98 100644
--- a/frontend/src/types/Graph.ts
+++ b/frontend/src/types/Graph.ts
@@ -332,8 +332,8 @@ export interface GraphWorkloadStatus {
   name: string;
   desiredReplicas: number;
   currentReplicas: number;
-  availableReplicas: number
-  syncedProxies: number
+  availableReplicas: number;
+  syncedProxies: number;
 }
 
 export interface GraphNodeAppHealth {
diff --git a/frontend/src/types/IstioConfigDetails.ts b/frontend/src/types/IstioConfigDetails.ts
index 195450e4a..d4a642581 100644
--- a/frontend/src/types/IstioConfigDetails.ts
+++ b/frontend/src/types/IstioConfigDetails.ts
@@ -17,7 +17,9 @@ import {
   EnvoyFilter,
   WorkloadGroup,
   References,
-  HelpMessage, WasmPlugin, Telemetry
+  HelpMessage,
+  WasmPlugin,
+  Telemetry
 } from './IstioObjects';
 import { AceOptions } from 'react-ace/types';
 
diff --git a/frontend/src/types/IstioConfigList.ts b/frontend/src/types/IstioConfigList.ts
index 77aa50091..1bb39f39b 100644
--- a/frontend/src/types/IstioConfigList.ts
+++ b/frontend/src/types/IstioConfigList.ts
@@ -216,7 +216,7 @@ export const toIstioItems = (istioConfigList: IstioConfigList): IstioConfigItem[
     }
 
     if (!entries) {
-      return
+      return;
     }
 
     entries.forEach(entry => {
@@ -324,8 +324,13 @@ export const gwToIstioItems = (gws: Gateway[], vss: VirtualService[], validation
   return istioItems;
 };
 
-export const k8sGwToIstioItems = (gws: K8sGateway[], k8srs: K8sHTTPRoute[]): IstioConfigItem[] => {
+export const k8sGwToIstioItems = (
+  gws: K8sGateway[],
+  k8srs: K8sHTTPRoute[],
+  validations: Validations
+): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
+  const hasValidations = (vKey: string) => validations.k8sgateway && validations.k8sgateway[vKey];
   const k8sGateways = new Set();
 
   const typeNameProto = dicIstioType['k8sgateways']; // ex. serviceEntries -> ServiceEntry
@@ -344,14 +349,14 @@ export const k8sGwToIstioItems = (gws: K8sGateway[], k8srs: K8sHTTPRoute[]): Ist
 
   gws.forEach(gw => {
     if (k8sGateways.has(gw.metadata.namespace + '/' + gw.metadata.name)) {
+      const vKey = validationKey(gw.metadata.name, gw.metadata.namespace);
       const item = {
         namespace: gw.metadata.namespace || '',
         type: typeName,
         name: gw.metadata.name,
         creationTimestamp: gw.metadata.creationTimestamp,
         resourceVersion: gw.metadata.resourceVersion,
-        // @TODO Validations
-        validation: undefined
+        validation: hasValidations(vKey) ? validations.k8sgateway[vKey] : undefined
       };
       item[entryName] = gw;
       istioItems.push(item);
@@ -384,22 +389,23 @@ export const seToIstioItems = (see: ServiceEntry[], validations: Validations): I
   return istioItems;
 };
 
-export const k8sHTTPRouteToIstioItems = (routes: K8sHTTPRoute[]): IstioConfigItem[] => {
+export const k8sHTTPRouteToIstioItems = (routes: K8sHTTPRoute[], validations: Validations): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
+  const hasValidations = (vKey: string) => validations.k8shttproute && validations.k8shttproute[vKey];
 
   const typeNameProto = dicIstioType['k8shttproutes']; // ex. serviceEntries -> ServiceEntry
   const typeName = typeNameProto.toLowerCase(); // ex. ServiceEntry -> serviceentry
   const entryName = typeNameProto.charAt(0).toLowerCase() + typeNameProto.slice(1);
 
   routes.forEach(route => {
+    const vKey = validationKey(route.metadata.name, route.metadata.namespace);
     const item = {
       namespace: route.metadata.namespace || '',
       type: typeName,
       name: route.metadata.name,
       creationTimestamp: route.metadata.creationTimestamp,
       resourceVersion: route.metadata.resourceVersion,
-      // @TODO Validations
-      validation: undefined
+      validation: hasValidations(vKey) ? validations.k8shttproute[vKey] : undefined
     };
     item[entryName] = route;
     istioItems.push(item);
diff --git a/frontend/src/types/IstioObjects.ts b/frontend/src/types/IstioObjects.ts
index f1d92127d..1ced39c93 100644
--- a/frontend/src/types/IstioObjects.ts
+++ b/frontend/src/types/IstioObjects.ts
@@ -2,7 +2,7 @@ import Namespace from './Namespace';
 import { ServicePort } from './ServiceInfo';
 import { ProxyStatus } from './Health';
 import { TimeInSeconds } from './Common';
-import { KIALI_RELATED_LABEL, KIALI_WIZARD_LABEL } from "components/IstioWizards/WizardActions";
+import { KIALI_RELATED_LABEL, KIALI_WIZARD_LABEL } from 'components/IstioWizards/WizardActions';
 import { PFColorVal } from 'components/Pf/PfColors';
 
 // Common types
@@ -556,7 +556,7 @@ export interface DestinationRule extends IstioObject {
 }
 
 export class DestinationRuleC implements DestinationRule {
-  metadata: K8sMetadata = {name: ''};
+  metadata: K8sMetadata = { name: '' };
   spec: DestinationRuleSpec = {};
 
   constructor(dr: DestinationRule) {
@@ -568,7 +568,11 @@ export class DestinationRuleC implements DestinationRule {
   }
 
   hasPeerAuthentication(): string {
-    if (!!this.metadata && !!this.metadata.annotations && this.metadata.annotations[KIALI_RELATED_LABEL] !== undefined) {
+    if (
+      !!this.metadata &&
+      !!this.metadata.annotations &&
+      this.metadata.annotations[KIALI_RELATED_LABEL] !== undefined
+    ) {
       const anno = this.metadata.annotations[KIALI_RELATED_LABEL];
       const parts = anno.split('/');
       if (parts.length > 1) {
@@ -655,12 +659,15 @@ export interface VirtualService extends IstioObject {
   spec: VirtualServiceSpec;
 }
 
-export function getWizardUpdateLabel(vs: VirtualService | VirtualService[] | null, k8sr: K8sHTTPRoute | K8sHTTPRoute[] | null) {
-  let label = getVirtualServiceUpdateLabel(vs)
+export function getWizardUpdateLabel(
+  vs: VirtualService | VirtualService[] | null,
+  k8sr: K8sHTTPRoute | K8sHTTPRoute[] | null
+) {
+  let label = getVirtualServiceUpdateLabel(vs);
   if (label === '') {
-    label = getK8sHTTPRouteUpdateLabel(k8sr)
+    label = getK8sHTTPRouteUpdateLabel(k8sr);
   }
-  return label
+  return label;
 }
 
 export function getVirtualServiceUpdateLabel(vs: VirtualService | VirtualService[] | null) {
@@ -677,8 +684,7 @@ export function getVirtualServiceUpdateLabel(vs: VirtualService | VirtualService
     virtualService = vs;
   }
 
-  if (virtualService && virtualService.metadata.labels &&
-    virtualService.metadata.labels[KIALI_WIZARD_LABEL]) {
+  if (virtualService && virtualService.metadata.labels && virtualService.metadata.labels[KIALI_WIZARD_LABEL]) {
     return virtualService.metadata.labels[KIALI_WIZARD_LABEL];
   } else {
     return '';
@@ -699,8 +705,7 @@ export function getK8sHTTPRouteUpdateLabel(k8sr: K8sHTTPRoute | K8sHTTPRoute[] |
     k8sHTTPRoute = k8sr;
   }
 
-  if (k8sHTTPRoute && k8sHTTPRoute.metadata.labels &&
-    k8sHTTPRoute.metadata.labels[KIALI_WIZARD_LABEL]) {
+  if (k8sHTTPRoute && k8sHTTPRoute.metadata.labels && k8sHTTPRoute.metadata.labels[KIALI_WIZARD_LABEL]) {
     return k8sHTTPRoute.metadata.labels[KIALI_WIZARD_LABEL];
   } else {
     return '';
@@ -735,7 +740,7 @@ export function getK8sGatewaysAsList(k8sGws: K8sGateway[]): string[] {
   if (k8sGws) {
     return k8sGws.map(gateway => gateway.metadata.namespace + '/' + gateway.metadata.name).sort();
   } else {
-    return []
+    return [];
   }
 }
 
@@ -789,23 +794,28 @@ export interface K8sHTTPRouteSpec {
 }
 
 export interface K8sRouteRule {
-  matches?:     K8sHTTPRouteMatch[];
-  filters?:     K8sHTTPRouteFilter[];
+  matches?: K8sHTTPRouteMatch[];
+  filters?: K8sHTTPRouteFilter[];
   backendRefs?: K8sRouteBackendRef[];
 }
 
 export interface K8sRouteBackendRef {
-  name:       string;
-  weight?:    number;
-  port?:      number;
+  name: string;
+  weight?: number;
+  port?: number;
   namespace?: string;
-  filters?:   K8sHTTPRouteFilter[];
+  filters?: K8sHTTPRouteFilter[];
 }
 
 export interface K8sHTTPRouteFilter {
   requestRedirect?: K8sHTTPRouteRequestRedirect;
   requestHeaderModifier?: K8sHTTPHeaderFilter;
-  type?:            string;
+  requestMirror?: K8sHTTPRequestMirrorFilter;
+  type?: string;
+}
+
+export interface K8sHTTPRequestMirrorFilter {
+  backendRef?: K8sRouteBackendRef;
 }
 
 export interface K8sHTTPHeaderFilter {
@@ -829,7 +839,7 @@ export interface K8sHTTPRouteMatch {
 }
 
 export interface HTTPMatch {
-  type?:  string;
+  type?: string;
   name?: string;
   value?: string;
 }
@@ -902,6 +912,17 @@ export interface Server {
   tls?: ServerTLSSettings;
 }
 
+export interface ServerForm {
+  number: string;
+  protocol: string;
+  name: string;
+  hosts: string[];
+  tlsMode: string;
+  tlsServerCertificate: string;
+  tlsPrivateKey: string;
+  tlsCaCertificate: string;
+}
+
 // 1.6
 export interface ServerPort {
   number: number;
diff --git a/frontend/src/types/IstioStatus.ts b/frontend/src/types/IstioStatus.ts
index ea7532d22..5095578e0 100644
--- a/frontend/src/types/IstioStatus.ts
+++ b/frontend/src/types/IstioStatus.ts
@@ -15,4 +15,4 @@ export interface ComponentStatus {
 export interface IstiodResourceThresholds {
   memory: number;
   cpu: number;
-}
\ No newline at end of file
+}
diff --git a/frontend/src/types/Permissions.ts b/frontend/src/types/Permissions.ts
index 3f2b1776b..88806a193 100644
--- a/frontend/src/types/Permissions.ts
+++ b/frontend/src/types/Permissions.ts
@@ -1,4 +1,4 @@
-import { serverConfig } from "../config";
+import { serverConfig } from '../config';
 
 export interface ResourcePermissions {
   create: boolean;
@@ -6,7 +6,6 @@ export interface ResourcePermissions {
   delete: boolean;
 }
 
-
 export function canCreate(privs?: ResourcePermissions) {
   return privs !== undefined && privs.create && !serverConfig.deployment.viewOnlyMode;
 }
diff --git a/frontend/src/types/ServerConfig.ts b/frontend/src/types/ServerConfig.ts
index e9fd9a28c..337811a49 100644
--- a/frontend/src/types/ServerConfig.ts
+++ b/frontend/src/types/ServerConfig.ts
@@ -109,6 +109,8 @@ export interface ToleranceConfig {
 */
 
 export interface ServerConfig {
+  accessibleNamespaces: Array<string>;
+  authStrategy: string;
   clusterInfo?: ClusterInfo;
   clusters: { [key: string]: MeshCluster };
   deployment: DeploymentConfig;
@@ -121,6 +123,7 @@ export interface ServerConfig {
   istioNamespace: string;
   istioLabels: { [key in IstioLabelKey]: string };
   kialiFeatureFlags: KialiFeatureFlags;
+  logLevel: string;
   prometheus: {
     globalScrapeInterval?: DurationInSeconds;
     storageTsdbRetention?: DurationInSeconds;
diff --git a/frontend/src/types/ServiceInfo.ts b/frontend/src/types/ServiceInfo.ts
index f468aa0f3..f38b0330f 100644
--- a/frontend/src/types/ServiceInfo.ts
+++ b/frontend/src/types/ServiceInfo.ts
@@ -1,7 +1,8 @@
 import { DEGRADED, FAILURE, HEALTHY, NA, ServiceHealth, Status } from './Health';
 import {
   DestinationRule,
-  getWizardUpdateLabel, K8sHTTPRoute,
+  getWizardUpdateLabel,
+  K8sHTTPRoute,
   ObjectCheck,
   ObjectValidation,
   ServiceEntry,
@@ -12,8 +13,8 @@ import {
 import { TLSStatus } from './TLSStatus';
 import { AdditionalItem } from './Workload';
 import { ResourcePermissions } from './Permissions';
-import {KIALI_WIZARD_LABEL} from "../components/IstioWizards/WizardActions";
-import {ServiceOverview} from "./ServiceList";
+import { KIALI_WIZARD_LABEL } from '../components/IstioWizards/WizardActions';
+import { ServiceOverview } from './ServiceList';
 
 export interface ServicePort {
   name: string;
@@ -81,8 +82,16 @@ export function getServiceDetailsUpdateLabel(serviceDetails: ServiceDetailsInfo
 }
 
 export function hasServiceDetailsTrafficRouting(serviceDetails: ServiceDetailsInfo | null);
-export function hasServiceDetailsTrafficRouting(vsList: VirtualService[], drList: DestinationRule[], routeList?: K8sHTTPRoute[]);
-export function hasServiceDetailsTrafficRouting(serviceDetailsOrVsList: ServiceDetailsInfo | VirtualService[] | null, drList?: DestinationRule[], routeList?: K8sHTTPRoute[]) {
+export function hasServiceDetailsTrafficRouting(
+  vsList: VirtualService[],
+  drList: DestinationRule[],
+  routeList?: K8sHTTPRoute[]
+);
+export function hasServiceDetailsTrafficRouting(
+  serviceDetailsOrVsList: ServiceDetailsInfo | VirtualService[] | null,
+  drList?: DestinationRule[],
+  routeList?: K8sHTTPRoute[]
+) {
   let virtualServicesList: VirtualService[];
   let destinationRulesList: DestinationRule[];
   let httpRoutesList: K8sHTTPRoute[];
@@ -99,7 +108,6 @@ export function hasServiceDetailsTrafficRouting(serviceDetailsOrVsList: ServiceD
     virtualServicesList = serviceDetailsOrVsList.virtualServices;
     destinationRulesList = serviceDetailsOrVsList.destinationRules;
     httpRoutesList = serviceDetailsOrVsList.k8sHTTPRoutes;
-
   }
 
   return virtualServicesList.length > 0 || destinationRulesList.length > 0 || httpRoutesList.length > 0;
@@ -166,8 +174,7 @@ export const globalChecks = (object: ObjectValidation): ObjectCheck[] => {
 };
 
 export function getServiceWizardLabel(serviceDetails: Service): string {
-  if (serviceDetails && serviceDetails.labels &&
-    serviceDetails.labels[KIALI_WIZARD_LABEL]) {
+  if (serviceDetails && serviceDetails.labels && serviceDetails.labels[KIALI_WIZARD_LABEL]) {
     return serviceDetails.labels[KIALI_WIZARD_LABEL];
   } else {
     return '';
@@ -177,7 +184,7 @@ export function getServiceWizardLabel(serviceDetails: Service): string {
 export function getServicePort(ports: { [key: string]: number }): number {
   let port = 80;
   if (ports) {
-    port = Object.values(ports)[0]
+    port = Object.values(ports)[0];
   }
   return port;
 }
diff --git a/frontend/src/types/StatusState.ts b/frontend/src/types/StatusState.ts
index ba302c7a3..c3a7915fe 100644
--- a/frontend/src/types/StatusState.ts
+++ b/frontend/src/types/StatusState.ts
@@ -18,6 +18,7 @@ export interface ExternalServiceInfo {
 
 export interface IstioEnvironment {
   isMaistra: boolean;
+  istioAPIEnabled: boolean;
 }
 
 export interface StatusState {
diff --git a/frontend/src/types/TLSStatus.ts b/frontend/src/types/TLSStatus.ts
index 46e541ce3..817054597 100644
--- a/frontend/src/types/TLSStatus.ts
+++ b/frontend/src/types/TLSStatus.ts
@@ -4,6 +4,7 @@ export enum MTLSStatuses {
   ENABLED_EXTENDED = 'MTLS_ENABLED_EXTENDED',
   PARTIALLY = 'MTLS_PARTIALLY_ENABLED',
   PARTIALLY_DEFAULT = 'MTLS_PARTIALLY_ENABLED_DEFAULT',
+  AUTO_DEFAULT = 'AUTO_MTLS_DEFAULT',
   NOT_ENABLED = 'MTLS_NOT_ENABLED',
   DISABLED = 'MTLS_DISABLED'
 }
diff --git a/frontend/src/types/__testData__/HealthConfig.ts b/frontend/src/types/__testData__/HealthConfig.ts
index 13ba09fe5..9f25612bb 100644
--- a/frontend/src/types/__testData__/HealthConfig.ts
+++ b/frontend/src/types/__testData__/HealthConfig.ts
@@ -1,8 +1,11 @@
 import { getExpr } from '../../config/HealthConfig';
 
 export const healthConfig = {
+  accessibleNamespaces: [],
+  authStrategy: '',
   clusters: {},
   gatewayAPIEnabled: false,
+  logLevel: '',
   kialiFeatureFlags: {
     certificatesInformationIndicators: {
       enabled: true
diff --git a/frontend/src/utils/IstioConfigUtils.ts b/frontend/src/utils/IstioConfigUtils.ts
index 9b617212c..247869e68 100644
--- a/frontend/src/utils/IstioConfigUtils.ts
+++ b/frontend/src/utils/IstioConfigUtils.ts
@@ -1,5 +1,12 @@
 import { IstioConfigDetails } from '../types/IstioConfigDetails';
-import { ConnectionPoolSettings, IstioObject, ObjectCheck, OutlierDetection, StatusCondition, Validations } from '../types/IstioObjects';
+import {
+  ConnectionPoolSettings,
+  IstioObject,
+  ObjectCheck,
+  OutlierDetection,
+  StatusCondition,
+  Validations
+} from '../types/IstioObjects';
 import _ from 'lodash';
 import { IstioConfigItem } from 'types/IstioConfigList';
 
@@ -185,7 +192,7 @@ export const hasMissingAuthPolicy = (workloadName: string, validations: Validati
   }
 
   return hasMissingAP;
-}
+};
 
 export const getReconciliationCondition = (
   istioConfigDetails?: IstioConfigDetails | IstioConfigItem
diff --git a/frontend/src/utils/RateIntervals.ts b/frontend/src/utils/RateIntervals.ts
index 3a824e2f8..9c2a7778f 100644
--- a/frontend/src/utils/RateIntervals.ts
+++ b/frontend/src/utils/RateIntervals.ts
@@ -1,6 +1,6 @@
-import { config } from "../config";
+import { config } from '../config';
 import { serverConfig } from '../config/ServerConfig';
-import { IntervalInMilliseconds } from "../types/Common";
+import { IntervalInMilliseconds } from '../types/Common';
 
 export const getName = (durationSeconds: number): string => {
   const name = serverConfig.durations[durationSeconds];
@@ -12,5 +12,5 @@ export const getName = (durationSeconds: number): string => {
 
 export const getRefreshIntervalName = (refreshInterval: IntervalInMilliseconds): string => {
   const refreshIntervalOption = config.toolbar.refreshInterval[refreshInterval];
-  return refreshIntervalOption.replace("Every ", "");
-}
+  return refreshIntervalOption.replace('Every ', '');
+};
diff --git a/frontend/src/utils/SearchParamUtils.ts b/frontend/src/utils/SearchParamUtils.ts
index 494e93bf1..3fc01edbd 100644
--- a/frontend/src/utils/SearchParamUtils.ts
+++ b/frontend/src/utils/SearchParamUtils.ts
@@ -7,7 +7,7 @@ export const getKioskMode = () => {
     return kioskParam;
   }
   return '';
-}
+};
 
 export const isKioskMode = () => {
   return getKioskMode() !== '';
diff --git a/frontend/src/utils/VictoryChartsUtils.ts b/frontend/src/utils/VictoryChartsUtils.ts
index 7955a3802..45c44e2b4 100644
--- a/frontend/src/utils/VictoryChartsUtils.ts
+++ b/frontend/src/utils/VictoryChartsUtils.ts
@@ -27,7 +27,9 @@ export const toVCDatapoints = (dps: Datapoint[], name: string): VCDataPoint[] =>
 };
 
 export const toVCSinglePoint = (dps: Datapoint[], name: string): VCDataPoint[] => {
-  const last = dps.filter(dp => !isNaN(dp[1]) && (dp[2] === undefined || !isNaN(dp[2]))).reduce((p, c) => (c[0] > p[0] ? c : p));
+  const last = dps
+    .filter(dp => !isNaN(dp[1]) && (dp[2] === undefined || !isNaN(dp[2])))
+    .reduce((p, c) => (c[0] > p[0] ? c : p));
   if (last) {
     return [
       {
@@ -69,8 +71,11 @@ export const toVCLines = (
     metrics.forEach(metric => {
       let reporter = metric.labels.reporter;
       let labelsNoReporter = { ...metric.labels };
-      delete labelsNoReporter.reporter
-      let labelsStr = Object.keys(labelsNoReporter).map(k => `${k}=${labelsNoReporter[k]}`).sort().join(',');
+      delete labelsNoReporter.reporter;
+      let labelsStr = Object.keys(labelsNoReporter)
+        .map(k => `${k}=${labelsNoReporter[k]}`)
+        .sort()
+        .join(',');
       labelsStr = `name=${metric.name},stat=${metric.stat}%` + labelsStr;
 
       if (!pairedMetrics[labelsStr]) {
@@ -87,8 +92,10 @@ export const toVCLines = (
       let name: string = '';
       if (twoLines[0] !== undefined && twoLines[1] !== undefined) {
         name = twoLines[0].name;
-        const minDatapointsLength = twoLines[0].datapoints.length < twoLines[1].datapoints.length ?
-          twoLines[0].datapoints.length : twoLines[1].datapoints.length;
+        const minDatapointsLength =
+          twoLines[0].datapoints.length < twoLines[1].datapoints.length
+            ? twoLines[0].datapoints.length
+            : twoLines[1].datapoints.length;
 
         for (let j = 0, sourceIdx = 0, destIdx = 0; j < minDatapointsLength; j++) {
           if (twoLines[0].datapoints[sourceIdx][0] !== twoLines[1].datapoints[destIdx][0]) {
@@ -106,7 +113,7 @@ export const toVCLines = (
           datapoints.push([
             twoLines[0].datapoints[sourceIdx][0],
             twoLines[0].datapoints[sourceIdx][1],
-            twoLines[1].datapoints[destIdx][1],
+            twoLines[1].datapoints[destIdx][1]
           ]);
           sourceIdx++;
           destIdx++;
@@ -114,23 +121,22 @@ export const toVCLines = (
       } else if (twoLines[0] !== undefined) {
         // Assign zero value to "y0" to denote "no data" for the destination reporter
         name = twoLines[0].name;
-        datapoints = twoLines[0].datapoints.map(d => ([d[0], d[1], 0]));
+        datapoints = twoLines[0].datapoints.map(d => [d[0], d[1], 0]);
       } else if (twoLines[1] !== undefined) {
         // Assign zero value to "y" to denote "no data" for the source reporter
         name = twoLines[1].name;
-        datapoints = twoLines[1].datapoints.map(d => ([d[0], 0, d[1]]));
+        datapoints = twoLines[1].datapoints.map(d => [d[0], 0, d[1]]);
       }
 
-      const dps =
-        xAxis === 'time' ? toVCDatapoints(datapoints, name) : toVCSinglePoint(datapoints, name);
-      return buildVCLine(dps, {name: name, unit: unit, color: color});
+      const dps = xAxis === 'time' ? toVCDatapoints(datapoints, name) : toVCSinglePoint(datapoints, name);
+      return buildVCLine(dps, { name: name, unit: unit, color: color });
     });
   } else {
     return metrics.map((line, i) => {
       const color = colors[i % colors.length];
       const dps =
         xAxis === 'time' ? toVCDatapoints(line.datapoints, line.name) : toVCSinglePoint(line.datapoints, line.name);
-      return buildVCLine(dps, {name: line.name, unit: unit, color: color});
+      return buildVCLine(dps, { name: line.name, unit: unit, color: color });
     });
   }
 };
diff --git a/frontend/src/utils/tracing/__tests__/TraceStats.test.ts b/frontend/src/utils/tracing/__tests__/TraceStats.test.ts
index 8af72d533..59fb42db0 100644
--- a/frontend/src/utils/tracing/__tests__/TraceStats.test.ts
+++ b/frontend/src/utils/tracing/__tests__/TraceStats.test.ts
@@ -130,7 +130,7 @@ describe('TraceStats.reduceMetricsStats', () => {
 
     expect(reduced.isComplete).toBe(true);
     expect(reduced.matrix).toHaveLength(4);
-    reduced.matrix.forEach(line => expect(line).toHaveLength(3));  // because there are 3 intervals, only 2 of which are filled
+    reduced.matrix.forEach(line => expect(line).toHaveLength(3)); // because there are 3 intervals, only 2 of which are filled
     // For each matrix cell (stat_x_duration), the reduced matrix contains
     // the average of "span duration - corresponding metric stat" for all 3 spans
     expect(reduced.matrix[0][0]).toBeCloseTo(0, 5); // 10m avg => average of [3-3, 3-3, 1-1]
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 2be777661..14a3b193d 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -3613,11 +3613,6 @@
     "@webassemblyjs/ast" "1.11.1"
     "@xtuc/long" "4.2.2"
 
-"@webcomponents/custom-elements@1.2.4":
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.2.4.tgz#7074543155396114617722724d6f6cb7b3800a14"
-  integrity sha512-WiTlgz6/kuwajYIcgyq64rSlCtb2AvbxwwrExP3wr6rKbJ72I3hi/sb4KdGUumfC+isDn2F0btZGk4MnWpyO1Q==
-
 "@wojtekmaj/enzyme-adapter-react-17@^0.6.7":
   version "0.6.7"
   resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.7.tgz#7784bd32f518b186218cebb26c98c852676f30b0"
diff --git a/go.mod b/go.mod
index 4e43347db..ec0368b5f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,11 @@
 module github.com/kiali/kiali
 
-go 1.18
+go 1.19
 
 require (
 	github.com/NYTimes/gziphandler v1.1.1
 	github.com/golang/protobuf v1.5.2
 	github.com/gorilla/mux v1.8.0
-	github.com/hashicorp/go-version v1.4.0
 	github.com/mitchellh/mapstructure v1.4.3
 	github.com/nitishm/engarde v0.1.1
 	github.com/openshift/api v0.0.0-20200221181648-8ce0047d664f
diff --git a/go.sum b/go.sum
index 5a336c534..bc1095377 100644
--- a/go.sum
+++ b/go.sum
@@ -210,8 +210,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4=
-github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
diff --git a/graph/telemetry/istio/appender/istio_details.go b/graph/telemetry/istio/appender/istio_details.go
index e743a5d46..f7a1f4d3c 100644
--- a/graph/telemetry/istio/appender/istio_details.go
+++ b/graph/telemetry/istio/appender/istio_details.go
@@ -7,7 +7,7 @@ import (
 
 	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
 	"k8s.io/apimachinery/pkg/labels"
-	k8s_networking_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
@@ -240,7 +240,7 @@ func decorateMatchingGateways(gwCrd *networking_v1beta1.Gateway, gatewayNodeMapp
 	}
 }
 
-func decorateMatchingAPIGateways(gwCrd *k8s_networking_v1alpha2.Gateway, gatewayNodeMapping map[*models.WorkloadListItem][]*graph.Node, nodeMetadataKey graph.MetadataKey) {
+func decorateMatchingAPIGateways(gwCrd *k8s_networking_v1beta1.Gateway, gatewayNodeMapping map[*models.WorkloadListItem][]*graph.Node, nodeMetadataKey graph.MetadataKey) {
 	gwSelector := labels.Set(gwCrd.Labels).AsSelector()
 	for gw, nodes := range gatewayNodeMapping {
 		if gwSelector.Matches(labels.Set(gw.Labels)) {
@@ -383,8 +383,8 @@ func (a IstioAppender) getIstioGatewayResources(globalInfo *graph.AppenderGlobal
 	return retVal
 }
 
-func (a IstioAppender) getGatewayAPIResources(globalInfo *graph.AppenderGlobalInfo) []*k8s_networking_v1alpha2.Gateway {
-	retVal := []*k8s_networking_v1alpha2.Gateway{}
+func (a IstioAppender) getGatewayAPIResources(globalInfo *graph.AppenderGlobalInfo) []*k8s_networking_v1beta1.Gateway {
+	retVal := []*k8s_networking_v1beta1.Gateway{}
 	for namespace := range a.AccessibleNamespaces {
 		istioCfg, err := globalInfo.Business.IstioConfig.GetIstioConfigList(context.TODO(), business.IstioConfigCriteria{
 			IncludeK8sGateways: true,
diff --git a/graph/types.go b/graph/types.go
index 85fc97f38..8b69921bb 100644
--- a/graph/types.go
+++ b/graph/types.go
@@ -209,11 +209,12 @@ func Id(cluster, serviceNamespace, service, workloadNamespace, workload, app, ve
 	// handle workload graph nodes (service graphs are initially processed as workload graphs)
 	if graphType == GraphTypeWorkload || graphType == GraphTypeService {
 		// workload graph nodes are type workload or service
-		if !workloadOk && !serviceOk {
-			panic(fmt.Sprintf("Failed ID gen2: cluster=[%s] namespace=[%s] workload=[%s] app=[%s] version=[%s] service=[%s] graphType=[%s]", cluster, namespace, workload, app, version, service, graphType))
-		}
 		if !workloadOk {
-			return fmt.Sprintf("svc_%s_%s_%s", cluster, namespace, service), NodeTypeService
+			if serviceOk {
+				return fmt.Sprintf("svc_%s_%s_%s", cluster, namespace, service), NodeTypeService
+			}
+			// We have seen cases when app is set but workload is unknown and service may be unknown or not set (See #5696)
+			return fmt.Sprintf("svc_%s_%s_%s", cluster, namespace, Unknown), NodeTypeService
 		}
 		return fmt.Sprintf("wl_%s_%s_%v", cluster, namespace, workload), NodeTypeWorkload
 	}
diff --git a/hack/istio/install-istio-via-istioctl.sh b/hack/istio/install-istio-via-istioctl.sh
index 45c875709..27dba5a9c 100755
--- a/hack/istio/install-istio-via-istioctl.sh
+++ b/hack/istio/install-istio-via-istioctl.sh
@@ -24,6 +24,8 @@ ISTIOCTL=
 ISTIO_DIR=
 ISTIO_EGRESSGATEWAY_ENABLED="true"
 ISTIO_INGRESSGATEWAY_ENABLED="true"
+K8S_GATEWAY_API_ENABLED="false"
+K8S_GATEWAY_API_VERSION=""
 ISTIO_VERSION=""
 MESH_ID=""
 MTLS="true"
@@ -101,6 +103,19 @@ while [[ $# -gt 0 ]]; do
       fi
       shift;shift
       ;;
+    -gae|--k8s-gateway-api-enabled)
+      if [ "${2}" == "true" ] || [ "${2}" == "false" ]; then
+        K8S_GATEWAY_API_ENABLED="$2"
+      else
+        echo "ERROR: The --k8s-gateway-api-enabled flag must be 'true' or 'false'"
+        exit 1
+      fi
+      shift;shift
+      ;;
+    -gav|--k8s-gateway-api-version)
+      K8S_GATEWAY_API_VERSION="$2"
+      shift;shift
+      ;;
     -iv|--istio-version)
       ISTIO_VERSION="$2"
       shift;shift
@@ -188,6 +203,11 @@ Valid command line arguments:
   -iie|--istio-ingressgateway-enabled (true|false)
        When set to true, istio-ingressgateway will be installed.
        Default: true
+  -gae|--k8s-gateway-api-enabled (true|false)
+       When set to true, K8s Gateway API will be installed.
+       Default: false
+  -gav|--k8s-gateway-api-version <version>:
+       The K8s Gateway API version to install. This is considered when --k8s-gateway-api-enabled is specified as "true".
   -ih|--image-hub <hub id>
        The hub where the Istio images will be pulled from.
        You can set this to "default" in order to use the default hub that the Istio charts use but
@@ -390,12 +410,10 @@ if [ "${DELETE_ISTIO}" == "true" ]; then
   echo Deleting Core Istio
   if [ "${PURGE_UNINSTALL}" == "true" ]; then
     echo "Purging all Istio resources"
-    # Although the 'uninstall' command has been available since Istio 1.7 and is listed in the official istio doc,
-    # it's still available in 'experimental' (x) scope, so it might change in the future.
     # The optional --purge flag will remove all Istio resources, including cluster-scoped resources that may be shared with other Istio control planes.
-    ${ISTIOCTL} x uninstall --purge -y
+    ${ISTIOCTL} uninstall --purge -y
   else
-    ${ISTIOCTL} manifest generate --set profile=${CONFIG_PROFILE} ${MANIFEST_CONFIG_SETTINGS_TO_APPLY} | ${CLIENT_EXE} delete -f -
+    ${ISTIOCTL} manifest generate --set profile=${CONFIG_PROFILE} ${MANIFEST_CONFIG_SETTINGS_TO_APPLY} | ${CLIENT_EXE} delete --ignore-not-found=true -f -
   fi
   if [[ "${CLIENT_EXE}" = *"oc" ]]; then
     echo "===== IMPORTANT ====="
@@ -435,6 +453,15 @@ else
     done
   done
 
+  if [ "${K8S_GATEWAY_API_ENABLED}" == "true" ]; then
+    if [ "${K8S_GATEWAY_API_VERSION}" == "" ]; then
+      echo "Gateway API Version is not specified, taking the latest released version"
+      K8S_GATEWAY_API_VERSION=`curl --head --silent "https://github.com/kubernetes-sigs/gateway-api/releases/latest" | grep "location: " | awk '{print $2}' | sed "s/.*tag\///g" | cat -v | sed "s/\^M//g"`
+    fi
+    echo "Installing Gateway API version ${K8S_GATEWAY_API_VERSION}"
+    ${CLIENT_EXE} apply -k "github.com/kubernetes-sigs/gateway-api/config/crd?ref=${K8S_GATEWAY_API_VERSION}"
+  fi
+
   # Do some OpenShift specific things
   if [[ "${CLIENT_EXE}" = *"oc" ]]; then
     if [ "${ISTIO_INGRESSGATEWAY_ENABLED}" == "true" ]; then
diff --git a/hack/istio/multicluster/deploy-kiali.sh b/hack/istio/multicluster/deploy-kiali.sh
index 20fa7c798..5d5f67076 100755
--- a/hack/istio/multicluster/deploy-kiali.sh
+++ b/hack/istio/multicluster/deploy-kiali.sh
@@ -14,7 +14,11 @@ if [ "${KIALI_ENABLED}" != "true" ]; then
   echo "Will not install kiali"
   return 0
 else
-  echo "Installing Kiali in the two clusters"
+  if [ "${SINGLE_KIALI}" == "true" ]; then
+    echo "Installing Kiali in a single cluster."
+  else
+    echo "Installing Kiali in the two clusters"
+  fi
 fi
 
 if ! which helm; then
@@ -26,17 +30,47 @@ deploy_kiali() {
   local helm_args=""
   if [ "${IS_OPENSHIFT}" == "true" ]; then
     helm_args="--disable-openapi-validation"
+    if [ "${KIALI_USE_DEV_IMAGE}" == "true" ]; then
+      echo "'--kiali-use-dev-image true' is only supported with minikube today - will not install Kiali"
+      return 1
+    fi
   fi
 
-  local web_fqdn="${1}"
-  local web_schema="${2}"
+  local cluster_name="${1}"
+  local web_fqdn="${2}"
+  local web_schema="${3}"
   [ ! -z "${web_fqdn}" ] && helm_args="--set server.web_fqdn=${web_fqdn} ${helm_args}"
   [ ! -z "${web_schema}" ] && helm_args="--set server.web_schema=${web_schema} ${helm_args}"
 
+  local minikube_ip=$(minikube ip -p "${cluster_name}")
+  local minikube_ip_dashed=$(minikube ip -p "${cluster_name}" | sed 's/\./-/g')
+  local kube_hostname="keycloak-${minikube_ip_dashed}.nip.io"
+
+  if [ "${KIALI_USE_DEV_IMAGE}" == "true" ]; then
+    local image_to_tag="quay.io/kiali/kiali:dev"
+    local image_to_push="${minikube_ip}:5000/kiali/kiali:dev"
+    echo "Tagging the dev image [${image_to_tag}] -> [${image_to_push}]..."
+    ${DORP} tag ${image_to_tag} ${image_to_push}
+    echo "Pushing the dev image [${image_to_push}] to the cluster [${cluster_name}]..."
+    ${DORP} push --tls-verify=false ${image_to_push}
+    helm_args="--set deployment.image_name=localhost:5000/kiali/kiali --set deployment.image_version=dev ${helm_args}"
+  fi
+
+  # These need to exist prior to installing Kiali.
+  # Create secret with the oidc secret
+  ${CLIENT_EXE} create configmap kiali-cabundle --from-file="openid-server-ca.crt=${KEYCLOAK_CERTS_DIR}/root-ca.pem" -n "${ISTIO_NAMESPACE}"
+  ${CLIENT_EXE} create secret generic kiali --from-literal="oidc-secret=kube-client-secret" -n istio-system
+  ${CLIENT_EXE} create clusterrolebinding kiali-user-viewer --clusterrole=kiali-viewer --user=oidc:kiali
+
+  # use the latest published server helm chart (if using dev images, it is up to the user to make sure this chart works with the dev image)
   helm upgrade --install                 \
     ${helm_args}                         \
     --namespace ${ISTIO_NAMESPACE}       \
-    --set auth.strategy="anonymous"      \
+    --set kubernetes_config.cache_enabled="false" \
+    --set auth.strategy="openid"      \
+    --set auth.openid.client_id="kube"      \
+    --set auth.openid.issuer_uri="https://${kube_hostname}/realms/kube" \
+    --set deployment.ingress.enabled="true" \
     --repo https://kiali.org/helm-charts \
     kiali-server                         \
     kiali-server
@@ -44,8 +78,11 @@ deploy_kiali() {
 
 echo "==== DEPLOY KIALI TO CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
 switch_cluster "${CLUSTER1_CONTEXT}" "${CLUSTER1_USER}" "${CLUSTER1_PASS}"
-deploy_kiali "${KIALI1_WEB_FQDN}" "${KIALI1_WEB_SCHEMA}"
+deploy_kiali "${CLUSTER1_NAME}" "${KIALI1_WEB_FQDN}" "${KIALI1_WEB_SCHEMA}"
 
-echo "==== DEPLOY KIALI TO CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
-switch_cluster "${CLUSTER2_CONTEXT}" "${CLUSTER2_USER}" "${CLUSTER2_PASS}"
-deploy_kiali "${KIALI2_WEB_FQDN}" "${KIALI2_WEB_SCHEMA}"
+if [ "${SINGLE_KIALI}" != "true" ]
+then
+  echo "==== DEPLOY KIALI TO CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
+  switch_cluster "${CLUSTER2_CONTEXT}" "${CLUSTER2_USER}" "${CLUSTER2_PASS}"
+  deploy_kiali "${CLUSTER2_NAME}" "${KIALI2_WEB_FQDN}" "${KIALI2_WEB_SCHEMA}"
+fi
diff --git a/hack/istio/multicluster/env.sh b/hack/istio/multicluster/env.sh
index eb72b6347..84b6bca9d 100755
--- a/hack/istio/multicluster/env.sh
+++ b/hack/istio/multicluster/env.sh
@@ -49,7 +49,7 @@ CLIENT_EXE_NAME="kubectl"
 ISTIO_DIR=""
 
 # If the scripts need image registry client, this is it (docker or podman)
-DORP="${DORP:-docker}"
+DORP="${DORP:-podman}"
 
 # The namespace where Istio will be found - this namespace must be the same on both clusters
 ISTIO_NAMESPACE="istio-system"
@@ -61,15 +61,19 @@ ISTIO_TAG=""
 
 # Certs directory where you want the generates cert files to be written
 CERTS_DIR="/tmp/istio-multicluster-certs"
+KEYCLOAK_CERTS_DIR=${CERTS_DIR}/keycloak
 
 # The default Mesh and Network identifiers
 MESH_ID="mesh-hack"
 NETWORK1_ID="network-east"
 NETWORK2_ID="network-west"
 
+# Deploy a single kiali or a kiali per cluster
+SINGLE_KIALI="true"
+
 # If a gateway is required to cross the networks, set this to true and one will be created
 # See: https://istio.io/latest/docs/setup/install/multicluster/multi-primary_multi-network/
-CROSSNETWORK_GATEWAY_REQUIRED="true"
+CROSSNETWORK_GATEWAY_REQUIRED="false"
 
 # Under some conditions, manually configuring the mesh network will be required.
 MANUAL_MESH_NETWORK_CONFIG=""
@@ -90,9 +94,12 @@ CLUSTER2_USER="kiali"
 CLUSTER2_PASS="kiali"
 
 # Should Kiali be installed? This installs the last release of Kiali via the kiali-server helm chart.
-# If you want another verison or your own dev build, you must disable this and install what you want manually.
+# If you want another version, you must disable this and install what you want manually.
 KIALI_ENABLED="true"
 
+# When installing Kiali, this will determine if a released image is used or if a local dev image is to be pushed and used.
+KIALI_USE_DEV_IMAGE="false"
+
 # Should Bookinfo demo be installed? If so, where?
 BOOKINFO_ENABLED="true"
 BOOKINFO_NAMESPACE="bookinfo"
@@ -191,6 +198,11 @@ while [[ $# -gt 0 ]]; do
       KIALI_ENABLED="$2"
       shift;shift
       ;;
+    -kudi|--kiali-use-dev-image)
+      [ "${2:-}" != "true" -a "${2:-}" != "false" ] && echo "--kiali-use-dev-image must be 'true' or 'false'" && exit 1
+      KIALI_USE_DEV_IMAGE="$2"
+      shift;shift
+      ;;
     -k1wf|--kiali1-web-fqdn)
       KIALI1_WEB_FQDN="$2"
       shift;shift
@@ -252,6 +264,10 @@ while [[ $# -gt 0 ]]; do
       NETWORK2_ID="$2"
       shift;shift
       ;;
+    -sk|--single-kiali)
+      SINGLE_KIALI="$2"
+      shift;shift
+      ;;
     -h|--help)
       cat <<HELPMSG
 Valid command line arguments:
@@ -275,6 +291,11 @@ Valid command line arguments:
   -ke|--kiali-enabled <bool>: If "true" the latest release of Kiali will be installed in both clusters. If you want
                               a different version of Kiali installed, you must set this to "false" and install it yourself.
                               (Default: true)
+  -kudi|--kiali-use-dev-image: If "true" the local dev image of Kiali will be pushed and used in the Kiali deployment.
+                               The local dev image must be tagged as "quay.io/kiali/kiali:dev" prior to running this script;
+                               that will be the image pushed to the clusters. You can "make container-build-kiali" to build it.
+                               Will be ignored if --kiali-enabled is 'false'. (Default: false)
+                               CURRENTLY ONLY SUPPORTED WITH MINIKUBE!
   -k1wf|--kiali1-web-fqdn <fqdn>: If specified, this will be the #1 Kaili setting for spec.server.web_fqdn.
   -k1ws|--kiali1-web-schema <schema>: If specified, this will be the #1 Kaili setting for spec.server.web_schema.
   -k2wf|--kiali2-web-fqdn <fqdn>: If specified, this will be the #2 Kaili setting for spec.server.web_fqdn.
@@ -291,6 +312,7 @@ Valid command line arguments:
   -n1|--network1 <id>: When Istio is installed in cluster 1, it will be part of the network with this given name. (Default: network-default)
   -n2|--network2 <id>: When Istio is installed in cluster 2, it will be part of the network with this given name.
                        If this is left as empty string, it will be the same as --network1. (Default: "")
+  -sk|--single-kiali <bool>: If "true", a single kiali will be deployed for the whole mesh. (Default: true)
   -h|--help: this message
 HELPMSG
       exit 1
@@ -434,6 +456,7 @@ export BOOKINFO_ENABLED \
        ISTIO_NAMESPACE \
        ISTIO_TAG \
        KIALI_ENABLED \
+       KIALI_USE_DEV_IMAGE \
        MANAGE_KIND \
        MANAGE_MINIKUBE \
        MANUAL_MESH_NETWORK_CONFIG \
@@ -443,7 +466,8 @@ export BOOKINFO_ENABLED \
        MINIKUBE_MEMORY \
        MESH_ID \
        NETWORK1_ID \
-       NETWORK2_ID
+       NETWORK2_ID \
+       SINGLE_KIALI
 
 cat <<EOM
 === SETTINGS ===
@@ -465,6 +489,7 @@ ISTIO_DIR=$ISTIO_DIR
 ISTIO_NAMESPACE=$ISTIO_NAMESPACE
 ISTIO_TAG=$ISTIO_TAG
 KIALI_ENABLED=$KIALI_ENABLED
+KIALI_USE_DEV_IMAGE=$KIALI_USE_DEV_IMAGE
 KIALI1_WEB_FQDN=$KIALI1_WEB_FQDN
 KIALI1_WEB_SCHEMA=$KIALI1_WEB_SCHEMA
 KIALI2_WEB_FQDN=$KIALI2_WEB_FQDN
@@ -479,6 +504,7 @@ MINIKUBE_MEMORY=$MINIKUBE_MEMORY
 MESH_ID=$MESH_ID
 NETWORK1_ID=$NETWORK1_ID
 NETWORK2_ID=$NETWORK2_ID
+SINGLE_KIALI=$SINGLE_KIALI
 === SETTINGS ===
 EOM
 
diff --git a/hack/istio/multicluster/realm-export-template.json b/hack/istio/multicluster/realm-export-template.json
new file mode 100644
index 000000000..bdb2e11d9
--- /dev/null
+++ b/hack/istio/multicluster/realm-export-template.json
@@ -0,0 +1,2194 @@
+{
+  "id": "e0ac20c5-93f2-4e17-9372-174c837a2090",
+  "realm": "kube",
+  "notBefore": 0,
+  "defaultSignatureAlgorithm": "RS256",
+  "revokeRefreshToken": false,
+  "refreshTokenMaxReuse": 0,
+  "accessTokenLifespan": 300,
+  "accessTokenLifespanForImplicitFlow": 900,
+  "ssoSessionIdleTimeout": 1800,
+  "ssoSessionMaxLifespan": 36000,
+  "ssoSessionIdleTimeoutRememberMe": 0,
+  "ssoSessionMaxLifespanRememberMe": 0,
+  "offlineSessionIdleTimeout": 2592000,
+  "offlineSessionMaxLifespanEnabled": false,
+  "offlineSessionMaxLifespan": 5184000,
+  "clientSessionIdleTimeout": 0,
+  "clientSessionMaxLifespan": 0,
+  "clientOfflineSessionIdleTimeout": 0,
+  "clientOfflineSessionMaxLifespan": 0,
+  "accessCodeLifespan": 60,
+  "accessCodeLifespanUserAction": 300,
+  "accessCodeLifespanLogin": 1800,
+  "actionTokenGeneratedByAdminLifespan": 43200,
+  "actionTokenGeneratedByUserLifespan": 300,
+  "oauth2DeviceCodeLifespan": 600,
+  "oauth2DevicePollingInterval": 5,
+  "enabled": true,
+  "sslRequired": "external",
+  "registrationAllowed": false,
+  "registrationEmailAsUsername": false,
+  "rememberMe": false,
+  "verifyEmail": false,
+  "loginWithEmailAllowed": true,
+  "duplicateEmailsAllowed": false,
+  "resetPasswordAllowed": false,
+  "editUsernameAllowed": false,
+  "bruteForceProtected": false,
+  "permanentLockout": false,
+  "maxFailureWaitSeconds": 900,
+  "minimumQuickLoginWaitSeconds": 60,
+  "waitIncrementSeconds": 60,
+  "quickLoginCheckMilliSeconds": 1000,
+  "maxDeltaTimeSeconds": 43200,
+  "failureFactor": 30,
+  "roles": {
+    "realm": [
+      {
+        "id": "64db5c26-096d-4a56-b2ab-9ed4c852342c",
+        "name": "offline_access",
+        "description": "${role_offline-access}",
+        "composite": false,
+        "clientRole": false,
+        "containerId": "e0ac20c5-93f2-4e17-9372-174c837a2090",
+        "attributes": {}
+      },
+      {
+        "id": "d5c64936-4bb2-48cb-8073-6353cfb33e5d",
+        "name": "uma_authorization",
+        "description": "${role_uma_authorization}",
+        "composite": false,
+        "clientRole": false,
+        "containerId": "e0ac20c5-93f2-4e17-9372-174c837a2090",
+        "attributes": {}
+      },
+      {
+        "id": "c1e7b782-836b-408e-a605-668521b6eeba",
+        "name": "default-roles-kube",
+        "description": "${role_default-roles}",
+        "composite": true,
+        "composites": {
+          "realm": [
+            "offline_access",
+            "uma_authorization"
+          ]
+        },
+        "clientRole": false,
+        "containerId": "e0ac20c5-93f2-4e17-9372-174c837a2090",
+        "attributes": {}
+      }
+    ],
+    "client": {
+      "realm-management": [
+        {
+          "id": "f426d525-406d-4786-bb92-968e01f1d8b3",
+          "name": "impersonation",
+          "description": "${role_impersonation}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "628339ee-6426-4c39-ad07-9ae6aa48f53d",
+          "name": "view-users",
+          "description": "${role_view-users}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "realm-management": [
+                "query-users",
+                "query-groups"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "e95eeec5-eee9-4e98-8bdd-54b04fc47e24",
+          "name": "view-authorization",
+          "description": "${role_view-authorization}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "371eeb31-041c-4d37-bc5b-332ec6041ed6",
+          "name": "manage-clients",
+          "description": "${role_manage-clients}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "57d9e8df-5711-4671-8752-43654ec65231",
+          "name": "query-clients",
+          "description": "${role_query-clients}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "e6ab384c-0711-4a39-927a-19519d939851",
+          "name": "manage-identity-providers",
+          "description": "${role_manage-identity-providers}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "c22e6513-b378-4eef-92f8-5aaa8be4a03a",
+          "name": "query-users",
+          "description": "${role_query-users}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "b4f7ebda-2796-4349-b5a1-542e442cb231",
+          "name": "query-realms",
+          "description": "${role_query-realms}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "e9db844d-4243-45fe-806b-3c5a5f48a418",
+          "name": "query-groups",
+          "description": "${role_query-groups}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "db805aab-e33f-4c4b-8589-0a6e29029ed2",
+          "name": "realm-admin",
+          "description": "${role_realm-admin}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "realm-management": [
+                "impersonation",
+                "view-users",
+                "view-authorization",
+                "manage-clients",
+                "query-clients",
+                "manage-identity-providers",
+                "query-users",
+                "query-realms",
+                "query-groups",
+                "view-identity-providers",
+                "create-client",
+                "manage-authorization",
+                "view-clients",
+                "view-realm",
+                "manage-realm",
+                "view-events",
+                "manage-users",
+                "manage-events"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "76d99306-3dfa-47ab-b03a-c770218f6b5b",
+          "name": "view-identity-providers",
+          "description": "${role_view-identity-providers}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "e0a931a4-39e3-4856-af71-91a429a9431c",
+          "name": "create-client",
+          "description": "${role_create-client}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "08fb1dd9-e05f-4b71-af4a-a0b4c1356f34",
+          "name": "manage-authorization",
+          "description": "${role_manage-authorization}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "4b67b4da-3936-442e-892f-99ac25e0cc75",
+          "name": "view-clients",
+          "description": "${role_view-clients}",
+          "composite": true,
+          "composites": {
+            "client": {
+              "realm-management": [
+                "query-clients"
+              ]
+            }
+          },
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "87062ebd-10a6-49a8-9e67-77e811f55b59",
+          "name": "view-realm",
+          "description": "${role_view-realm}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "a494f325-5db4-4643-879f-80fbfa73b0ed",
+          "name": "manage-realm",
+          "description": "${role_manage-realm}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "365b29b5-abb5-4f92-b588-2e525d276d76",
+          "name": "view-events",
+          "description": "${role_view-events}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "5a799229-4ac4-4618-ae91-96df4228bb4e",
+          "name": "manage-users",
+          "description": "${role_manage-users}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        },
+        {
+          "id": "5733b595-d97b-40ab-be3d-0b5a48f141f9",
+          "name": "manage-events",
+          "description": "${role_manage-events}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+          "attributes": {}
+        }
+      ],
+      "security-admin-console": [],
+      "admin-cli": [],
+      "account-console": [],
+      "broker": [],
+      "account": [
+        {
+          "id": "1722d0d2-209a-4096-b99f-0d2cb7ebb3ee",
+          "name": "view-groups",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "ee102ab9-ed9b-4d91-a3f9-256ca374de72",
+          "attributes": {}
+        },
+        {
+          "id": "b3af48da-9f5e-4443-aa3f-9db93ee9ba6c",
+          "name": "delete-account",
+          "description": "${role_delete-account}",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "ee102ab9-ed9b-4d91-a3f9-256ca374de72",
+          "attributes": {}
+        },
+        {
+          "id": "07cdb160-b740-4bc8-a139-c1fd250f8bdd",
+          "name": "manage-account",
+          "composite": false,
+          "clientRole": true,
+          "containerId": "ee102ab9-ed9b-4d91-a3f9-256ca374de72",
+          "attributes": {}
+        }
+      ],
+      "kube": []
+    }
+  },
+  "groups": [],
+  "defaultRole": {
+    "id": "c1e7b782-836b-408e-a605-668521b6eeba",
+    "name": "default-roles-kube",
+    "description": "${role_default-roles}",
+    "composite": true,
+    "clientRole": false,
+    "containerId": "e0ac20c5-93f2-4e17-9372-174c837a2090"
+  },
+  "requiredCredentials": [
+    "password"
+  ],
+  "otpPolicyType": "totp",
+  "otpPolicyAlgorithm": "HmacSHA1",
+  "otpPolicyInitialCounter": 0,
+  "otpPolicyDigits": 6,
+  "otpPolicyLookAheadWindow": 1,
+  "otpPolicyPeriod": 30,
+  "otpPolicyCodeReusable": false,
+  "otpSupportedApplications": [
+    "totpAppFreeOTPName",
+    "totpAppGoogleName"
+  ],
+  "webAuthnPolicyRpEntityName": "keycloak",
+  "webAuthnPolicySignatureAlgorithms": [
+    "ES256"
+  ],
+  "webAuthnPolicyRpId": "",
+  "webAuthnPolicyAttestationConveyancePreference": "not specified",
+  "webAuthnPolicyAuthenticatorAttachment": "not specified",
+  "webAuthnPolicyRequireResidentKey": "not specified",
+  "webAuthnPolicyUserVerificationRequirement": "not specified",
+  "webAuthnPolicyCreateTimeout": 0,
+  "webAuthnPolicyAvoidSameAuthenticatorRegister": false,
+  "webAuthnPolicyAcceptableAaguids": [],
+  "webAuthnPolicyPasswordlessRpEntityName": "keycloak",
+  "webAuthnPolicyPasswordlessSignatureAlgorithms": [
+    "ES256"
+  ],
+  "webAuthnPolicyPasswordlessRpId": "",
+  "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
+  "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
+  "webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
+  "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
+  "webAuthnPolicyPasswordlessCreateTimeout": 0,
+  "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
+  "webAuthnPolicyPasswordlessAcceptableAaguids": [],
+  "scopeMappings": [
+    {
+      "clientScope": "offline_access",
+      "roles": [
+        "offline_access"
+      ]
+    }
+  ],
+  "clientScopeMappings": {
+    "account": [
+      {
+        "client": "account-console",
+        "roles": [
+          "manage-account",
+          "view-groups"
+        ]
+      }
+    ]
+  },
+  "clients": [
+    {
+      "id": "ee102ab9-ed9b-4d91-a3f9-256ca374de72",
+      "clientId": "account",
+      "name": "${client_account}",
+      "rootUrl": "${authBaseUrl}",
+      "baseUrl": "/realms/kube/account/",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [
+        "/realms/kube/account/*"
+      ],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "1e832c40-8ac6-4c5d-a443-ecbdcfcd8346",
+      "clientId": "account-console",
+      "name": "${client_account-console}",
+      "rootUrl": "${authBaseUrl}",
+      "baseUrl": "/realms/kube/account/",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [
+        "/realms/kube/account/*"
+      ],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+",
+        "pkce.code.challenge.method": "S256"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "protocolMappers": [
+        {
+          "id": "b7d7586e-ea08-417a-8ac4-2e8fb6112887",
+          "name": "audience resolve",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-audience-resolve-mapper",
+          "consentRequired": false,
+          "config": {}
+        }
+      ],
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "1f0d24f8-a026-4e8b-8cc3-660fa252d543",
+      "clientId": "admin-cli",
+      "name": "${client_admin-cli}",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": false,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": true,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "f3d654a9-956a-4cf8-8444-e8e84f9222d0",
+      "clientId": "broker",
+      "name": "${client_broker}",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": true,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": false,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "1fb36f9e-3be3-4a38-80d3-5b25f91dfb35",
+      "clientId": "kube",
+      "name": "",
+      "description": "",
+      "rootUrl": "",
+      "adminUrl": "",
+      "baseUrl": "",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "secret": "kube-client-secret",
+      "redirectUris": [
+        "https://_KIALI_KUBE_HOSTNAME/kiali/*"
+      ],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": true,
+      "serviceAccountsEnabled": false,
+      "publicClient": false,
+      "frontchannelLogout": true,
+      "protocol": "openid-connect",
+      "attributes": {
+        "oidc.ciba.grant.enabled": "false",
+        "client.secret.creation.time": "1673459800",
+        "backchannel.logout.session.required": "true",
+        "post.logout.redirect.uris": "+",
+        "display.on.consent.screen": "false",
+        "oauth2.device.authorization.grant.enabled": "false",
+        "backchannel.logout.revoke.offline.tokens": "false"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": true,
+      "nodeReRegistrationTimeout": -1,
+      "protocolMappers": [
+        {
+          "id": "b50616f7-7077-4a69-b260-58302235e350",
+          "name": "Client IP Address",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usersessionmodel-note-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.session.note": "clientAddress",
+            "userinfo.token.claim": "true",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "clientAddress",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "b378d4aa-6535-4569-b57a-d67470271618",
+          "name": "Client ID",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usersessionmodel-note-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.session.note": "clientId",
+            "userinfo.token.claim": "true",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "clientId",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "f2013edb-5690-4ef3-bb5c-83767d41ff0d",
+          "name": "Client Host",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usersessionmodel-note-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.session.note": "clientHost",
+            "userinfo.token.claim": "true",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "clientHost",
+            "jsonType.label": "String"
+          }
+        }
+      ],
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "aa9ae24e-00ff-404d-bfc6-be35734cbbbd",
+      "clientId": "realm-management",
+      "name": "${client_realm-management}",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [],
+      "webOrigins": [],
+      "notBefore": 0,
+      "bearerOnly": true,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": false,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    },
+    {
+      "id": "3f526ead-d3e6-471e-b936-2353130aa169",
+      "clientId": "security-admin-console",
+      "name": "${client_security-admin-console}",
+      "rootUrl": "${authAdminUrl}",
+      "baseUrl": "/admin/kube/console/",
+      "surrogateAuthRequired": false,
+      "enabled": true,
+      "alwaysDisplayInConsole": false,
+      "clientAuthenticatorType": "client-secret",
+      "redirectUris": [
+        "/admin/kube/console/*"
+      ],
+      "webOrigins": [
+        "+"
+      ],
+      "notBefore": 0,
+      "bearerOnly": false,
+      "consentRequired": false,
+      "standardFlowEnabled": true,
+      "implicitFlowEnabled": false,
+      "directAccessGrantsEnabled": false,
+      "serviceAccountsEnabled": false,
+      "publicClient": true,
+      "frontchannelLogout": false,
+      "protocol": "openid-connect",
+      "attributes": {
+        "post.logout.redirect.uris": "+",
+        "pkce.code.challenge.method": "S256"
+      },
+      "authenticationFlowBindingOverrides": {},
+      "fullScopeAllowed": false,
+      "nodeReRegistrationTimeout": 0,
+      "protocolMappers": [
+        {
+          "id": "008efbef-a11e-4cfe-9a11-324155a26ad7",
+          "name": "locale",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "locale",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "locale",
+            "jsonType.label": "String"
+          }
+        }
+      ],
+      "defaultClientScopes": [
+        "web-origins",
+        "acr",
+        "profile",
+        "roles",
+        "email"
+      ],
+      "optionalClientScopes": [
+        "address",
+        "phone",
+        "offline_access",
+        "microprofile-jwt"
+      ]
+    }
+  ],
+  "clientScopes": [
+    {
+      "id": "613c16b8-0385-4921-9235-c20606e4dc5c",
+      "name": "phone",
+      "description": "OpenID Connect built-in scope: phone",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "display.on.consent.screen": "true",
+        "consent.screen.text": "${phoneScopeConsentText}"
+      },
+      "protocolMappers": [
+        {
+          "id": "2233df82-bf89-4c87-8e0c-2526a04bbe19",
+          "name": "phone number verified",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "phoneNumberVerified",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "phone_number_verified",
+            "jsonType.label": "boolean"
+          }
+        },
+        {
+          "id": "a61ea563-d187-4839-be31-6cb907997baf",
+          "name": "phone number",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "phoneNumber",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "phone_number",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    },
+    {
+      "id": "bb1a3a67-655e-4bb1-9923-9a984ceb14e6",
+      "name": "profile",
+      "description": "OpenID Connect built-in scope: profile",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "display.on.consent.screen": "true",
+        "consent.screen.text": "${profileScopeConsentText}"
+      },
+      "protocolMappers": [
+        {
+          "id": "113a5a90-9707-4c34-96a8-910b923c5aff",
+          "name": "full name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-full-name-mapper",
+          "consentRequired": false,
+          "config": {
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "userinfo.token.claim": "true"
+          }
+        },
+        {
+          "id": "f36c68d0-08e2-469a-a58c-1f6bd047f03f",
+          "name": "picture",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "picture",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "picture",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "f5de2a06-52e9-48e3-b011-26b4932f4cd5",
+          "name": "locale",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "locale",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "locale",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "f5cd39d3-b0bd-4115-9034-7b682f1eded1",
+          "name": "updated at",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "updatedAt",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "updated_at",
+            "jsonType.label": "long"
+          }
+        },
+        {
+          "id": "118eeec8-5df8-4376-9e3a-24e5c89f500f",
+          "name": "birthdate",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "birthdate",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "birthdate",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "866fdb9f-fd91-4b07-9190-3547d9d59938",
+          "name": "profile",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "profile",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "profile",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "8171c472-29b0-4b97-b6db-21038b6a39aa",
+          "name": "family name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "lastName",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "family_name",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "cadaeae7-f61d-4a76-a743-fdcb48f1b558",
+          "name": "username",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "username",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "preferred_username",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "499311aa-3cb8-4ab8-8102-3341337d0fab",
+          "name": "zoneinfo",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "zoneinfo",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "zoneinfo",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "567d29c2-9a9c-416f-9970-5b68b549054a",
+          "name": "gender",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "gender",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "gender",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "bba4f6a5-9d42-47c7-a862-ba8e79df6caa",
+          "name": "website",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "website",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "website",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "ad0e7c1c-3046-4f84-a1f9-c0e40963c33d",
+          "name": "nickname",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "nickname",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "nickname",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "5284a278-5291-42b7-a1ed-2a5b43b20cd9",
+          "name": "given name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "firstName",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "given_name",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "1ee88aa7-9c94-4e3b-8e94-a88ce7e197bd",
+          "name": "middle name",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-attribute-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "middleName",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "middle_name",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    },
+    {
+      "id": "7eb0c1fb-d72f-49f4-b084-e8709dc4f222",
+      "name": "web-origins",
+      "description": "OpenID Connect scope for add allowed web origins to the access token",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "false",
+        "display.on.consent.screen": "false",
+        "consent.screen.text": ""
+      },
+      "protocolMappers": [
+        {
+          "id": "746a3a6b-7d76-44d1-927b-378908fac9b8",
+          "name": "allowed web origins",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-allowed-origins-mapper",
+          "consentRequired": false,
+          "config": {}
+        }
+      ]
+    },
+    {
+      "id": "73278016-286a-4be6-b388-74472073b922",
+      "name": "roles",
+      "description": "OpenID Connect scope for add user roles to the access token",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "false",
+        "display.on.consent.screen": "true",
+        "consent.screen.text": "${rolesScopeConsentText}"
+      },
+      "protocolMappers": [
+        {
+          "id": "e0badb72-ef92-4f0a-87d1-da6f5c4ff011",
+          "name": "realm roles",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-realm-role-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.attribute": "foo",
+            "access.token.claim": "true",
+            "claim.name": "realm_access.roles",
+            "jsonType.label": "String",
+            "multivalued": "true"
+          }
+        },
+        {
+          "id": "ca057a4d-e3d1-4a74-a74f-a673bdee20f2",
+          "name": "audience resolve",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-audience-resolve-mapper",
+          "consentRequired": false,
+          "config": {}
+        },
+        {
+          "id": "3dda0255-b570-49f8-9a54-8354f8f239bf",
+          "name": "client roles",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-client-role-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.attribute": "foo",
+            "access.token.claim": "true",
+            "claim.name": "resource_access.${client_id}.roles",
+            "jsonType.label": "String",
+            "multivalued": "true"
+          }
+        }
+      ]
+    },
+    {
+      "id": "9f890827-9be5-4af7-9bc9-bad72a2569c2",
+      "name": "address",
+      "description": "OpenID Connect built-in scope: address",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "display.on.consent.screen": "true",
+        "consent.screen.text": "${addressScopeConsentText}"
+      },
+      "protocolMappers": [
+        {
+          "id": "69fd6b76-ba73-4fa0-9d37-ee443b7616d2",
+          "name": "address",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-address-mapper",
+          "consentRequired": false,
+          "config": {
+            "user.attribute.formatted": "formatted",
+            "user.attribute.country": "country",
+            "user.attribute.postal_code": "postal_code",
+            "userinfo.token.claim": "true",
+            "user.attribute.street": "street",
+            "id.token.claim": "true",
+            "user.attribute.region": "region",
+            "access.token.claim": "true",
+            "user.attribute.locality": "locality"
+          }
+        }
+      ]
+    },
+    {
+      "id": "a05c52ab-653d-478e-8be4-dae7a06e6904",
+      "name": "acr",
+      "description": "OpenID Connect scope for add acr (authentication context class reference) to the token",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "false",
+        "display.on.consent.screen": "false"
+      },
+      "protocolMappers": [
+        {
+          "id": "126b51b4-a1e4-4199-bfc3-84edfa4a4c42",
+          "name": "acr loa level",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-acr-mapper",
+          "consentRequired": false,
+          "config": {
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "userinfo.token.claim": "true"
+          }
+        }
+      ]
+    },
+    {
+      "id": "5ec2b0de-275e-40cb-ae98-8ba9f51dcce0",
+      "name": "role_list",
+      "description": "SAML role list",
+      "protocol": "saml",
+      "attributes": {
+        "consent.screen.text": "${samlRoleListScopeConsentText}",
+        "display.on.consent.screen": "true"
+      },
+      "protocolMappers": [
+        {
+          "id": "71ff760e-55e4-466b-9f0e-8876c1ee147b",
+          "name": "role list",
+          "protocol": "saml",
+          "protocolMapper": "saml-role-list-mapper",
+          "consentRequired": false,
+          "config": {
+            "single": "false",
+            "attribute.nameformat": "Basic",
+            "attribute.name": "Role"
+          }
+        }
+      ]
+    },
+    {
+      "id": "1dc49f7f-262a-444f-8ad0-728444443c67",
+      "name": "email",
+      "description": "OpenID Connect built-in scope: email",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "display.on.consent.screen": "true",
+        "consent.screen.text": "${emailScopeConsentText}"
+      },
+      "protocolMappers": [
+        {
+          "id": "ac8331c6-b4e0-4fab-b8ec-20a5d16a87c8",
+          "name": "email verified",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "emailVerified",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "email_verified",
+            "jsonType.label": "boolean"
+          }
+        },
+        {
+          "id": "3bfaf7b4-9676-4211-8ab9-2c420bd62484",
+          "name": "email",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "email",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "email",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    },
+    {
+      "id": "7b08e87f-1c10-4f10-afa6-b7ed20d4193b",
+      "name": "offline_access",
+      "description": "OpenID Connect built-in scope: offline_access",
+      "protocol": "openid-connect",
+      "attributes": {
+        "consent.screen.text": "${offlineAccessScopeConsentText}",
+        "display.on.consent.screen": "true"
+      }
+    },
+    {
+      "id": "7061acc2-6e27-48bb-9a77-26d8708970f9",
+      "name": "microprofile-jwt",
+      "description": "Microprofile - JWT built-in scope",
+      "protocol": "openid-connect",
+      "attributes": {
+        "include.in.token.scope": "true",
+        "display.on.consent.screen": "false"
+      },
+      "protocolMappers": [
+        {
+          "id": "b71002b8-6ad0-40c3-82cb-295661310ed4",
+          "name": "upn",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-property-mapper",
+          "consentRequired": false,
+          "config": {
+            "userinfo.token.claim": "true",
+            "user.attribute": "username",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "upn",
+            "jsonType.label": "String"
+          }
+        },
+        {
+          "id": "efacfd88-1fe3-45ad-a405-9efdd90940a5",
+          "name": "groups",
+          "protocol": "openid-connect",
+          "protocolMapper": "oidc-usermodel-realm-role-mapper",
+          "consentRequired": false,
+          "config": {
+            "multivalued": "true",
+            "userinfo.token.claim": "true",
+            "user.attribute": "foo",
+            "id.token.claim": "true",
+            "access.token.claim": "true",
+            "claim.name": "groups",
+            "jsonType.label": "String"
+          }
+        }
+      ]
+    }
+  ],
+  "defaultDefaultClientScopes": [
+    "role_list",
+    "profile",
+    "email",
+    "roles",
+    "web-origins",
+    "acr"
+  ],
+  "defaultOptionalClientScopes": [
+    "offline_access",
+    "address",
+    "phone",
+    "microprofile-jwt"
+  ],
+  "browserSecurityHeaders": {
+    "contentSecurityPolicyReportOnly": "",
+    "xContentTypeOptions": "nosniff",
+    "xRobotsTag": "none",
+    "xFrameOptions": "SAMEORIGIN",
+    "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+    "xXSSProtection": "1; mode=block",
+    "strictTransportSecurity": "max-age=31536000; includeSubDomains"
+  },
+  "smtpServer": {},
+  "eventsEnabled": false,
+  "eventsListeners": [
+    "jboss-logging"
+  ],
+  "enabledEventTypes": [],
+  "adminEventsEnabled": false,
+  "adminEventsDetailsEnabled": false,
+  "identityProviders": [],
+  "identityProviderMappers": [],
+  "components": {
+    "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
+      {
+        "id": "1d2fc24c-3194-4d3a-95c0-6e244c29c383",
+        "name": "Allowed Client Scopes",
+        "providerId": "allowed-client-templates",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "allow-default-scopes": [
+            "true"
+          ]
+        }
+      },
+      {
+        "id": "3ec54b35-9534-49e4-8707-1199f8714850",
+        "name": "Allowed Client Scopes",
+        "providerId": "allowed-client-templates",
+        "subType": "authenticated",
+        "subComponents": {},
+        "config": {
+          "allow-default-scopes": [
+            "true"
+          ]
+        }
+      },
+      {
+        "id": "547b699e-a139-42fc-b0cd-1da315ddf7a8",
+        "name": "Allowed Protocol Mapper Types",
+        "providerId": "allowed-protocol-mappers",
+        "subType": "authenticated",
+        "subComponents": {},
+        "config": {
+          "allowed-protocol-mapper-types": [
+            "oidc-sha256-pairwise-sub-mapper",
+            "saml-role-list-mapper",
+            "oidc-usermodel-attribute-mapper",
+            "saml-user-property-mapper",
+            "oidc-full-name-mapper",
+            "oidc-usermodel-property-mapper",
+            "saml-user-attribute-mapper",
+            "oidc-address-mapper"
+          ]
+        }
+      },
+      {
+        "id": "838505aa-5c49-43d7-b012-9ec715a8c7a2",
+        "name": "Trusted Hosts",
+        "providerId": "trusted-hosts",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "host-sending-registration-request-must-match": [
+            "true"
+          ],
+          "client-uris-must-match": [
+            "true"
+          ]
+        }
+      },
+      {
+        "id": "61a7f6f9-932e-49f2-be99-1863553b3306",
+        "name": "Max Clients Limit",
+        "providerId": "max-clients",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "max-clients": [
+            "200"
+          ]
+        }
+      },
+      {
+        "id": "9fdf90ed-9287-46c2-8672-e3dda59d6e03",
+        "name": "Consent Required",
+        "providerId": "consent-required",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {}
+      },
+      {
+        "id": "0e0941c5-0432-4da9-8253-6d72c8bc0a50",
+        "name": "Full Scope Disabled",
+        "providerId": "scope",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {}
+      },
+      {
+        "id": "1144ed38-5552-4e1e-8f49-e7db9dc5147b",
+        "name": "Allowed Protocol Mapper Types",
+        "providerId": "allowed-protocol-mappers",
+        "subType": "anonymous",
+        "subComponents": {},
+        "config": {
+          "allowed-protocol-mapper-types": [
+            "saml-user-attribute-mapper",
+            "oidc-usermodel-property-mapper",
+            "oidc-sha256-pairwise-sub-mapper",
+            "oidc-full-name-mapper",
+            "saml-role-list-mapper",
+            "oidc-address-mapper",
+            "oidc-usermodel-attribute-mapper",
+            "saml-user-property-mapper"
+          ]
+        }
+      }
+    ],
+    "org.keycloak.keys.KeyProvider": [
+      {
+        "id": "9ea06d19-febd-4cf9-a5c3-d33590493298",
+        "name": "rsa-generated",
+        "providerId": "rsa-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ]
+        }
+      },
+      {
+        "id": "799abce3-0bc3-46ee-b9b3-0c03c2145f63",
+        "name": "hmac-generated",
+        "providerId": "hmac-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ],
+          "algorithm": [
+            "HS256"
+          ]
+        }
+      },
+      {
+        "id": "399ed6a3-f2f0-4d38-9e96-fb0dbb65fa99",
+        "name": "aes-generated",
+        "providerId": "aes-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ]
+        }
+      },
+      {
+        "id": "61d651b9-33e3-415d-8fc2-ac2a78293621",
+        "name": "rsa-enc-generated",
+        "providerId": "rsa-enc-generated",
+        "subComponents": {},
+        "config": {
+          "priority": [
+            "100"
+          ],
+          "algorithm": [
+            "RSA-OAEP"
+          ]
+        }
+      }
+    ]
+  },
+  "internationalizationEnabled": false,
+  "supportedLocales": [],
+  "authenticationFlows": [
+    {
+      "id": "398a2836-b2ef-4e06-bde1-c82125d1f7a3",
+      "alias": "Account verification options",
+      "description": "Method with which to verity the existing account",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-email-verification",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Verify Existing Account by Re-authentication",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "1887b0da-f780-4f85-a5f0-d31c68bfc2bd",
+      "alias": "Authentication Options",
+      "description": "Authentication options.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "basic-auth",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "basic-auth-otp",
+          "authenticatorFlow": false,
+          "requirement": "DISABLED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "auth-spnego",
+          "authenticatorFlow": false,
+          "requirement": "DISABLED",
+          "priority": 30,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "0400e6d0-5bd6-4f3e-ad61-9a1a1f7379f4",
+      "alias": "Browser - Conditional OTP",
+      "description": "Flow to determine if the OTP is required for the authentication",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "auth-otp-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "86b3bfcd-a288-45a4-9d5c-9d6deb25cb40",
+      "alias": "Direct Grant - Conditional OTP",
+      "description": "Flow to determine if the OTP is required for the authentication",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "direct-grant-validate-otp",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "dff1e9fc-02c8-42cf-9ac1-941da47a319b",
+      "alias": "First broker login - Conditional OTP",
+      "description": "Flow to determine if the OTP is required for the authentication",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "auth-otp-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "0c46c0ac-4373-4164-b0f9-05160cd7467e",
+      "alias": "Handle Existing Account",
+      "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-confirm-link",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Account verification options",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "6719b74c-7282-4820-9907-a3894df1a660",
+      "alias": "Reset - Conditional OTP",
+      "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "conditional-user-configured",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "reset-otp",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "0cf5bde2-1378-47d2-aae5-2b79d6136029",
+      "alias": "User creation or linking",
+      "description": "Flow for the existing/non-existing user alternatives",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticatorConfig": "create unique user config",
+          "authenticator": "idp-create-user-if-unique",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Handle Existing Account",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "ba87d858-f2bb-42c6-ae1b-a12c1294e5f1",
+      "alias": "Verify Existing Account by Re-authentication",
+      "description": "Reauthentication of existing account",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-username-password-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "First broker login - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "ac7d3941-ab8f-407e-bde1-269bf202acb6",
+      "alias": "browser",
+      "description": "browser based authentication",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "auth-cookie",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "auth-spnego",
+          "authenticatorFlow": false,
+          "requirement": "DISABLED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "identity-provider-redirector",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 25,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "ALTERNATIVE",
+          "priority": 30,
+          "autheticatorFlow": true,
+          "flowAlias": "forms",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "2e1f5295-b5ea-493b-9617-b2cb5fc5d67d",
+      "alias": "clients",
+      "description": "Base authentication for clients",
+      "providerId": "client-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "client-secret",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "client-jwt",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "client-secret-jwt",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 30,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "client-x509",
+          "authenticatorFlow": false,
+          "requirement": "ALTERNATIVE",
+          "priority": 40,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "202de361-648f-4eec-8605-f1c0becdf4af",
+      "alias": "direct grant",
+      "description": "OpenID Connect Resource Owner Grant",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "direct-grant-validate-username",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "direct-grant-validate-password",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 30,
+          "autheticatorFlow": true,
+          "flowAlias": "Direct Grant - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "05e3ee34-747f-49f1-bf8e-2dd7797a5581",
+      "alias": "docker auth",
+      "description": "Used by Docker clients to authenticate against the IDP",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "docker-http-basic-authenticator",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "e50edbb6-6333-4419-8dc5-5404bc075449",
+      "alias": "first broker login",
+      "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticatorConfig": "review profile config",
+          "authenticator": "idp-review-profile",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "User creation or linking",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "8512c867-7f09-4db9-8274-11464a9a9fdc",
+      "alias": "forms",
+      "description": "Username, password, otp and other auth forms.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "auth-username-password-form",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Browser - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "dea5ae28-c3ce-4617-9558-8ac7fe0762a6",
+      "alias": "http challenge",
+      "description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "no-cookie-redirect",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": true,
+          "flowAlias": "Authentication Options",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "b07af156-9dab-4770-b45d-f939dc59ad56",
+      "alias": "registration",
+      "description": "registration flow",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "registration-page-form",
+          "authenticatorFlow": true,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": true,
+          "flowAlias": "registration form",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "e29f7e22-fd21-4edc-8b9e-518594e2180e",
+      "alias": "registration form",
+      "description": "registration form",
+      "providerId": "form-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "registration-user-creation",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "registration-profile-action",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 40,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "registration-password-action",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 50,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "registration-recaptcha-action",
+          "authenticatorFlow": false,
+          "requirement": "DISABLED",
+          "priority": 60,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "4dd934f8-8f7f-4da1-84e2-55bd4209a667",
+      "alias": "reset credentials",
+      "description": "Reset credentials for a user if they forgot their password or something",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "reset-credentials-choose-user",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "reset-credential-email",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticator": "reset-password",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 30,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        },
+        {
+          "authenticatorFlow": true,
+          "requirement": "CONDITIONAL",
+          "priority": 40,
+          "autheticatorFlow": true,
+          "flowAlias": "Reset - Conditional OTP",
+          "userSetupAllowed": false
+        }
+      ]
+    },
+    {
+      "id": "c9aa7721-ceb3-44c1-ad0d-de2332f40251",
+      "alias": "saml ecp",
+      "description": "SAML ECP Profile Authentication Flow",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "http-basic-authenticator",
+          "authenticatorFlow": false,
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "autheticatorFlow": false,
+          "userSetupAllowed": false
+        }
+      ]
+    }
+  ],
+  "authenticatorConfig": [
+    {
+      "id": "0199c2ed-d39b-4cc6-9c6a-11b6eef6038f",
+      "alias": "create unique user config",
+      "config": {
+        "require.password.update.after.registration": "false"
+      }
+    },
+    {
+      "id": "152d9a57-64f2-4573-9dd7-4a62a8dcb08e",
+      "alias": "review profile config",
+      "config": {
+        "update.profile.on.first.login": "missing"
+      }
+    }
+  ],
+  "requiredActions": [
+    {
+      "alias": "CONFIGURE_TOTP",
+      "name": "Configure OTP",
+      "providerId": "CONFIGURE_TOTP",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 10,
+      "config": {}
+    },
+    {
+      "alias": "terms_and_conditions",
+      "name": "Terms and Conditions",
+      "providerId": "terms_and_conditions",
+      "enabled": false,
+      "defaultAction": false,
+      "priority": 20,
+      "config": {}
+    },
+    {
+      "alias": "UPDATE_PASSWORD",
+      "name": "Update Password",
+      "providerId": "UPDATE_PASSWORD",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 30,
+      "config": {}
+    },
+    {
+      "alias": "UPDATE_PROFILE",
+      "name": "Update Profile",
+      "providerId": "UPDATE_PROFILE",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 40,
+      "config": {}
+    },
+    {
+      "alias": "VERIFY_EMAIL",
+      "name": "Verify Email",
+      "providerId": "VERIFY_EMAIL",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 50,
+      "config": {}
+    },
+    {
+      "alias": "delete_account",
+      "name": "Delete Account",
+      "providerId": "delete_account",
+      "enabled": false,
+      "defaultAction": false,
+      "priority": 60,
+      "config": {}
+    },
+    {
+      "alias": "webauthn-register",
+      "name": "Webauthn Register",
+      "providerId": "webauthn-register",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 70,
+      "config": {}
+    },
+    {
+      "alias": "webauthn-register-passwordless",
+      "name": "Webauthn Register Passwordless",
+      "providerId": "webauthn-register-passwordless",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 80,
+      "config": {}
+    },
+    {
+      "alias": "update_user_locale",
+      "name": "Update User Locale",
+      "providerId": "update_user_locale",
+      "enabled": true,
+      "defaultAction": false,
+      "priority": 1000,
+      "config": {}
+    }
+  ],
+  "browserFlow": "browser",
+  "registrationFlow": "registration",
+  "directGrantFlow": "direct grant",
+  "resetCredentialsFlow": "reset credentials",
+  "clientAuthenticationFlow": "clients",
+  "dockerAuthenticationFlow": "docker auth",
+  "attributes": {
+    "cibaBackchannelTokenDeliveryMode": "poll",
+    "cibaExpiresIn": "120",
+    "cibaAuthRequestedUserHint": "login_hint",
+    "oauth2DeviceCodeLifespan": "600",
+    "clientOfflineSessionMaxLifespan": "0",
+    "oauth2DevicePollingInterval": "5",
+    "clientSessionIdleTimeout": "0",
+    "parRequestUriLifespan": "60",
+    "clientSessionMaxLifespan": "0",
+    "clientOfflineSessionIdleTimeout": "0",
+    "cibaInterval": "5",
+    "realmReusableOtpCode": "false"
+  },
+  "keycloakVersion": "20.0.2",
+  "userManagedAccessAllowed": false,
+  "clientProfiles": {
+    "profiles": []
+  },
+  "clientPolicies": {
+    "policies": []
+  }
+}
diff --git a/hack/istio/multicluster/start-minikube.sh b/hack/istio/multicluster/start-minikube.sh
index 65568a3cf..4e131e7da 100755
--- a/hack/istio/multicluster/start-minikube.sh
+++ b/hack/istio/multicluster/start-minikube.sh
@@ -10,9 +10,14 @@
 SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)"
 source ${SCRIPT_DIR}/env.sh $*
 
+# Remove old certs and recreate
+rm -rf "${KEYCLOAK_CERTS_DIR}"
+mkdir -p "${KEYCLOAK_CERTS_DIR}"
+
 start_minikube() {
   local profile="${1}"
   local lb_addrs="${2}"
+  local extra_config="${3:-}"
 
   if [ "${MINIKUBE_CPU}" != "" ]; then local cpu="--kubernetes-cpu ${MINIKUBE_CPU}"; fi
   if [ "${MINIKUBE_DISK}" != "" ]; then local disk="--kubernetes-disk ${MINIKUBE_DISK}"; fi
@@ -22,6 +27,7 @@ start_minikube() {
     --minikube-profile "${profile}"          \
     --load-balancer-addrs "${lb_addrs}"      \
     --kubernetes-driver "${MINIKUBE_DRIVER}" \
+    -mf "${extra_config}"                    \
     ${cpu:-} ${disk:-} ${mem:-}              \
     start
 
@@ -40,8 +46,133 @@ else
   exit 1
 fi
 
+# Generate root CA for keycloak/oidc.
+openssl genrsa -out "${KEYCLOAK_CERTS_DIR}"/root-ca-key.pem 2048
+
+openssl req -x509 -new -nodes -key "${KEYCLOAK_CERTS_DIR}"/root-ca-key.pem \
+  -days 3650 -sha256 -out "${KEYCLOAK_CERTS_DIR}"/root-ca.pem -subj "/CN=kube-ca"
+
+# First start a minikube cluster without kubernetes to both copy over the certs and get the IP
+echo "==== START MINIKUBE FOR CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT} without kubernetes"
+start_minikube "${CLUSTER1_NAME}" "70-84" "--no-kubernetes=true"
+
+MINIKUBE_KEYCLOAK_CERTS_DIR=/var/lib/minikube/certs/keycloak
+# Copy certs
+minikube ssh -p "${CLUSTER1_NAME}" sudo mkdir "${MINIKUBE_KEYCLOAK_CERTS_DIR}"
+for f in "${KEYCLOAK_CERTS_DIR}"/*
+do
+  echo "Copying ${f} to minikube"
+  minikube cp "${f}" "${MINIKUBE_KEYCLOAK_CERTS_DIR}/$(basename "${f}")" -p "${CLUSTER1_NAME}"
+done
+
+# Then get the IP
+MINIKUBE_IP_DASHED=$(minikube ip -p "${CLUSTER1_NAME}" | sed 's/\./-/g')
+KUBE_HOSTNAME="keycloak-${MINIKUBE_IP_DASHED}.nip.io"
+
+# Now generate the oidc server cert from the root CA
+cat <<EOF > "${KEYCLOAK_CERTS_DIR}"/req.cnf
+[req]
+req_extensions = v3_req
+distinguished_name = req_distinguished_name
+
+[req_distinguished_name]
+
+[ v3_req ]
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = ${KUBE_HOSTNAME}
+EOF
+
+# generate private key
+openssl genrsa -out "${KEYCLOAK_CERTS_DIR}"/key.pem 2048
+
+# create certificate signing request
+openssl req -new -key "${KEYCLOAK_CERTS_DIR}"/key.pem -out "${KEYCLOAK_CERTS_DIR}"/csr.pem \
+  -subj "/CN=kube-ca" \
+  -addext "subjectAltName = DNS:${KUBE_HOSTNAME}" \
+  -sha256 -config "${KEYCLOAK_CERTS_DIR}"/req.cnf
+
+# create certificate
+openssl x509 -req -in "${KEYCLOAK_CERTS_DIR}"/csr.pem \
+  -CA "${KEYCLOAK_CERTS_DIR}"/root-ca.pem -CAkey "${KEYCLOAK_CERTS_DIR}"/root-ca-key.pem \
+  -CAcreateserial -sha256 -out "${KEYCLOAK_CERTS_DIR}"/cert.pem -days 3650 \
+  -extensions v3_req -extfile "${KEYCLOAK_CERTS_DIR}"/req.cnf
+
+# Restart with kubernetes
+minikube stop -p "${CLUSTER1_NAME}"
 echo "==== START MINIKUBE FOR CLUSTER #1 [${CLUSTER1_NAME}] - ${CLUSTER1_CONTEXT}"
-start_minikube "${CLUSTER1_NAME}" "70-84"
+start_minikube "${CLUSTER1_NAME}" "70-84" "--extra-config=apiserver.oidc-issuer-url=https://${KUBE_HOSTNAME}/realms/kube --extra-config=apiserver.oidc-ca-file=${MINIKUBE_KEYCLOAK_CERTS_DIR}/root-ca.pem --extra-config=apiserver.oidc-client-id=kube --extra-config=apiserver.oidc-groups-claim=groups --extra-config=apiserver.oidc-username-prefix=oidc: --extra-config=apiserver.oidc-groups-prefix=oidc: --extra-config=apiserver.oidc-username-claim=preferred_username"
+
+# Wait for ingress to become ready before deploying keycloak since keycloak relies on it.
+${CLIENT_EXE} rollout status deployment/ingress-nginx-controller -n ingress-nginx
+
+helm upgrade --install --wait --timeout 15m \
+  --namespace keycloak --create-namespace \
+  --repo https://charts.bitnami.com/bitnami keycloak keycloak \
+  --reuse-values --values - <<EOF
+auth:
+  createAdminUser: true
+  adminUser: admin
+  adminPassword: admin
+  managementUser: manager
+  managementPassword: manager
+proxyAddressForwarding: true
+ingress:
+  enabled: true
+  hostname: ${KUBE_HOSTNAME}
+  annotations:
+    kubernetes.io/ingress.class: nginx
+  tls: true
+  extraTls:
+  - hosts:
+    - ${KUBE_HOSTNAME}
+    secretName: keycloak.kind.cluster-tls
+postgresql:
+  enabled: true
+  postgresqlPassword: password
+EOF
+
+# create secret used by keycloak ingress
+${CLIENT_EXE} create secret tls -n keycloak keycloak.kind.cluster-tls \
+  --cert="${KEYCLOAK_CERTS_DIR}"/cert.pem \
+  --key="${KEYCLOAK_CERTS_DIR}"/key.pem
+
+# Give the ingress some time to be ready
+# ${CLIENT_EXE} wait ingress/keycloak -n keycloak --context east --for=jsonpath='{.status.loadBalancer.ingress[*].hostname}'=localhost
+sleep 20
+
+# Get a token from keycloak to use the admin api
+TOKEN_KEY=$(curl -k -X POST https://"${KUBE_HOSTNAME}"/realms/master/protocol/openid-connect/token \
+            -d grant_type=password \
+            -d client_id=admin-cli \
+            -d username=admin \
+            -d password=admin \
+            -d scope=openid \
+            -d response_type=id_token | jq -r '.access_token')
+
+# Replace the redirect URI with the minikube ip. Create the realm.
+sed "s/_KIALI_KUBE_HOSTNAME/$(minikube ip -p "${CLUSTER1_NAME}")/g" "${SCRIPT_DIR}"/realm-export-template.json | curl -k -L https://"${KUBE_HOSTNAME}"/admin/realms -H "Authorization: Bearer $TOKEN_KEY" -H "Content-Type: application/json" -X POST -d @-
+
+# Create the kiali user
+curl -k -L https://"${KUBE_HOSTNAME}"/admin/realms/kube/users -H "Authorization: Bearer $TOKEN_KEY" -d '{"username": "kiali", "enabled": true, "credentials": [{"type": "password", "value": "kiali"}]}' -H 'Content-Type: application/json'
+
+# Now start the west cluster, copy over certs, restart with certs and options.
+# First start a minikube cluster without kubernetes to both copy over the certs and get the IP
+echo "==== START MINIKUBE FOR CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT} without kubernetes"
+start_minikube "${CLUSTER2_NAME}" "85-98" "--no-kubernetes=true --network=mk-${CLUSTER1_NAME}"
+
+# Copy certs
+minikube ssh -p "${CLUSTER2_NAME}" sudo mkdir "${MINIKUBE_KEYCLOAK_CERTS_DIR}"
+for f in "${KEYCLOAK_CERTS_DIR}"/*
+do
+  echo "Copying ${f} to minikube"
+  minikube cp "${f}" "${MINIKUBE_KEYCLOAK_CERTS_DIR}/$(basename "${f}")" -p "${CLUSTER2_NAME}"
+done
 
+# Restart with kubernetes
+minikube stop -p "${CLUSTER2_NAME}"
 echo "==== START MINIKUBE FOR CLUSTER #2 [${CLUSTER2_NAME}] - ${CLUSTER2_CONTEXT}"
-start_minikube "${CLUSTER2_NAME}" "85-98"
+start_minikube "${CLUSTER2_NAME}" "85-98" "--network=mk-${CLUSTER1_NAME} --extra-config=apiserver.oidc-issuer-url=https://${KUBE_HOSTNAME}/realms/kube --extra-config=apiserver.oidc-ca-file=${MINIKUBE_KEYCLOAK_CERTS_DIR}/root-ca.pem --extra-config=apiserver.oidc-client-id=kube --extra-config=apiserver.oidc-groups-claim=groups --extra-config=apiserver.oidc-username-prefix=oidc: --extra-config=apiserver.oidc-groups-prefix=oidc: --extra-config=apiserver.oidc-username-claim=preferred_username"
diff --git a/hack/k8s-minikube.sh b/hack/k8s-minikube.sh
index 26dbea3d0..ae44374b1 100755
--- a/hack/k8s-minikube.sh
+++ b/hack/k8s-minikube.sh
@@ -304,10 +304,6 @@ EOF
   ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create secret tls dex.example.com.tls --cert=${CERTS_PATH}/cert.pem --key=${CERTS_PATH}/key.pem -n dex
 
   ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- apply -n dex -f ${DEX_VERSION_PATH}/examples/k8s/dex.kiali.yaml
-  ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- get cm -n dex dex -o yaml | \
-    sed 's/name: dex/name: dex-oidc/g' | \
-    sed 's/responseTypes.*/responseTypes: ["id_token"]/g' | \
-    ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- apply -f -
   [ "$?" != "0" ] && echo "ERROR: Failed to install dex" && exit 1
   echo "Deploying oauth2 proxy..."
   ${MINIKUBE_EXEC_WITH_PROFILE} kubectl -- create -f ${DEX_VERSION_PATH}/examples/k8s/oauth2.proxy
@@ -644,6 +640,24 @@ debug "minikube is located at $(which ${MINIKUBE_EXE})"
 
 if [ "$_CMD" = "start" ]; then
   echo 'Starting minikube...'
+
+  # Check if no-kubernetes flag is present. If so, we shouldn't try to provide a kube version,
+  # start addons, or apply additional yaml
+  if grep -q 'no-kubernetes' <<< "${MINIKUBE_START_FLAGS}"
+  then
+    ${MINIKUBE_EXEC_WITH_PROFILE} start \
+      ${MINIKUBE_START_FLAGS} \
+      ${INSECURE_REGISTRY_START_ARG} \
+      --cpus=${K8S_CPU} \
+      --memory=${K8S_MEMORY} \
+      --disk-size=${K8S_DISK} \
+      --driver=${K8S_DRIVER}
+
+    [ "$?" != "0" ] && echo "ERROR: Failed to start minikube" && exit 1
+
+    exit 0
+  fi
+
   ${MINIKUBE_EXEC_WITH_PROFILE} start \
     ${MINIKUBE_START_FLAGS} \
     ${INSECURE_REGISTRY_START_ARG} \
diff --git a/hack/run-kiali.sh b/hack/run-kiali.sh
index 876d31197..333d122d8 100755
--- a/hack/run-kiali.sh
+++ b/hack/run-kiali.sh
@@ -650,7 +650,6 @@ start_port_forward_component() {
       infomsg "${COMPONENT_NAME} port-forward started (pid=${childpid}, job=${!PORT_FORWARD_JOB_VARNAME})"
       if ! curl ${EXPECTED_URL} > /dev/null 2>&1; then
         errormsg "Cannot port-forward to the ${COMPONENT_NAME} component. You must expose it and specify its URL via ${CMDLINE_OPT}"
-        cleanup_and_exit 1
       fi
     fi
   else
diff --git a/hack/setup-kind-in-ci.sh b/hack/setup-kind-in-ci.sh
index 3752fa346..b0d371ab4 100755
--- a/hack/setup-kind-in-ci.sh
+++ b/hack/setup-kind-in-ci.sh
@@ -138,7 +138,7 @@ LBCONFIGMAP
 
 infomsg "Installing istio"
 # Apparently you can't set the requests to zero for the proxy so just setting them to some really low number.
-hack/istio/install-istio-via-istioctl.sh --reduce-resources true --client-exe-path "$(which kubectl)" -cn "cluster-default" -mid "mesh-default" -net "network-default"
+hack/istio/install-istio-via-istioctl.sh --reduce-resources true --client-exe-path "$(which kubectl)" -cn "cluster-default" -mid "mesh-default" -net "network-default" -gae "true"
   
 infomsg "Pushing the images into the cluster..."
 make -e DORP="${DORP}" -e CLUSTER_TYPE="kind" -e KIND_NAME="ci" cluster-push-kiali
@@ -147,11 +147,16 @@ infomsg "Cloning kiali helm-charts..."
 git clone --single-branch --branch "${TARGET_BRANCH}" https://github.com/kiali/helm-charts.git helm-charts
 make -C helm-charts build-helm-charts
 
+HELM="helm-charts/_output/helm-install/helm"
+
+infomsg "Using helm: $(ls -l ${HELM})"
+infomsg "$(${HELM} version)"
+
 infomsg "Installing kiali server via Helm"
 # The grafana and tracing urls need to be set for backend e2e tests
 # but they don't need to be accessible outside the cluster.
 # Need a single dashboard set for grafana.
-helm install \
+${HELM} install \
   --namespace istio-system \
   --set auth.strategy="${AUTH_STRATEGY}" \
   --set deployment.logger.log_level="trace" \
@@ -173,7 +178,7 @@ helm install \
 
 # Create the citest service account whose token will be used to log into Kiali
 infomsg "Installing the test ServiceAccount with read-write permissions"
-for o in role rolebinding serviceaccount; do helm template --show-only "templates/${o}.yaml" --namespace=istio-system --set deployment.instance_name=citest --set auth.strategy=anonymous kiali-server helm-charts/_output/charts/kiali-server-*-SNAPSHOT.tgz; done | kubectl apply -f -
+for o in role rolebinding serviceaccount; do ${HELM} template --show-only "templates/${o}.yaml" --namespace=istio-system --set deployment.instance_name=citest --set auth.strategy=anonymous kiali-server helm-charts/_output/charts/kiali-server-*-SNAPSHOT.tgz; done | kubectl apply -f -
 
 # Unfortunately kubectl rollout status fails if the resource does not exist yet.
 for (( i=1; i<=60; i++ ))
@@ -211,7 +216,4 @@ if [ "${TIMEOUT}" == "True" ]; then
   exit 1
 fi
 
-infomsg "Kiali is ready."
-
-
-
+infomsg "Kiali is ready."
\ No newline at end of file
diff --git a/handlers/authentication.go b/handlers/authentication.go
index b4a9352fe..4d5abbaa4 100644
--- a/handlers/authentication.go
+++ b/handlers/authentication.go
@@ -7,6 +7,7 @@ import (
 
 	"k8s.io/client-go/tools/clientcmd/api"
 
+	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/business/authentication"
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
@@ -132,13 +133,19 @@ func AuthenticationInfo(w http.ResponseWriter, r *http.Request) {
 
 	switch conf.Auth.Strategy {
 	case config.AuthStrategyOpenshift:
-		business, err := getBusiness(r)
+		token, err := kubernetes.GetKialiToken()
+		if err != nil {
+			RespondWithDetailedError(w, http.StatusInternalServerError, "Error obtaining Kiali SA token", err.Error())
+			return
+		}
+
+		layer, err := business.Get(&api.AuthInfo{Token: token})
 		if err != nil {
 			RespondWithDetailedError(w, http.StatusInternalServerError, "Error authenticating (getting business layer)", err.Error())
 			return
 		}
 
-		metadata, err := business.OpenshiftOAuth.Metadata(r)
+		metadata, err := layer.OpenshiftOAuth.Metadata(r)
 		if err != nil {
 			RespondWithDetailedError(w, http.StatusInternalServerError, "Error trying to get OAuth metadata", err.Error())
 			return
diff --git a/handlers/config.go b/handlers/config.go
index 7640f785c..d8f379793 100644
--- a/handlers/config.go
+++ b/handlers/config.go
@@ -49,21 +49,24 @@ type DeploymentConfig struct {
 // PublicConfig is a subset of Kiali configuration that can be exposed to clients to
 // help them interact with the system.
 type PublicConfig struct {
-	ClusterInfo         ClusterInfo                 `json:"clusterInfo,omitempty"`
-	Clusters            map[string]business.Cluster `json:"clusters,omitempty"`
-	Deployment          DeploymentConfig            `json:"deployment,omitempty"`
-	GatewayAPIEnabled   bool                        `json:"gatewayAPIEnabled,omitempty"`
-	HealthConfig        config.HealthConfig         `json:"healthConfig,omitempty"`
-	InstallationTag     string                      `json:"installationTag,omitempty"`
-	IstioAnnotations    IstioAnnotations            `json:"istioAnnotations,omitempty"`
-	IstioCanaryRevision IstioCanaryRevision         `json:"istioCanaryRevision,omitempty"`
-	IstioStatusEnabled  bool                        `json:"istioStatusEnabled,omitempty"`
-	IstioIdentityDomain string                      `json:"istioIdentityDomain,omitempty"`
-	IstioNamespace      string                      `json:"istioNamespace,omitempty"`
-	IstioLabels         config.IstioLabels          `json:"istioLabels,omitempty"`
-	IstioConfigMap      string                      `json:"istioConfigMap"`
-	KialiFeatureFlags   config.KialiFeatureFlags    `json:"kialiFeatureFlags,omitempty"`
-	Prometheus          PrometheusConfig            `json:"prometheus,omitempty"`
+	AccessibleNamespaces []string                    `json:"accesibleNamespaces,omitempty"`
+	AuthStrategy         string                      `json:"authStrategy,omitempty"`
+	ClusterInfo          ClusterInfo                 `json:"clusterInfo,omitempty"`
+	Clusters             map[string]business.Cluster `json:"clusters,omitempty"`
+	Deployment           DeploymentConfig            `json:"deployment,omitempty"`
+	GatewayAPIEnabled    bool                        `json:"gatewayAPIEnabled,omitempty"`
+	HealthConfig         config.HealthConfig         `json:"healthConfig,omitempty"`
+	InstallationTag      string                      `json:"installationTag,omitempty"`
+	IstioAnnotations     IstioAnnotations            `json:"istioAnnotations,omitempty"`
+	IstioCanaryRevision  IstioCanaryRevision         `json:"istioCanaryRevision,omitempty"`
+	IstioStatusEnabled   bool                        `json:"istioStatusEnabled,omitempty"`
+	IstioIdentityDomain  string                      `json:"istioIdentityDomain,omitempty"`
+	IstioNamespace       string                      `json:"istioNamespace,omitempty"`
+	IstioLabels          config.IstioLabels          `json:"istioLabels,omitempty"`
+	IstioConfigMap       string                      `json:"istioConfigMap"`
+	KialiFeatureFlags    config.KialiFeatureFlags    `json:"kialiFeatureFlags,omitempty"`
+	LogLevel             string                      `json:"logLevel,omitempty"`
+	Prometheus           PrometheusConfig            `json:"prometheus,omitempty"`
 }
 
 // Config is a REST http.HandlerFunc serving up the Kiali configuration made public to clients.
@@ -75,7 +78,9 @@ func Config(w http.ResponseWriter, r *http.Request) {
 	promConfig := getPrometheusConfig()
 	config := config.Get()
 	publicConfig := PublicConfig{
-		Clusters: make(map[string]business.Cluster),
+		AccessibleNamespaces: config.Deployment.AccessibleNamespaces,
+		AuthStrategy:         config.Auth.Strategy,
+		Clusters:             make(map[string]business.Cluster),
 		Deployment: DeploymentConfig{
 			ViewOnlyMode: config.Deployment.ViewOnlyMode,
 		},
@@ -94,6 +99,7 @@ func Config(w http.ResponseWriter, r *http.Request) {
 			Upgrade: config.ExternalServices.Istio.IstioCanaryRevision.Upgrade,
 		},
 		KialiFeatureFlags: config.KialiFeatureFlags,
+		LogLevel:          log.GetLogLevel(),
 		Prometheus: PrometheusConfig{
 			GlobalScrapeInterval: promConfig.GlobalScrapeInterval,
 			StorageTsdbRetention: promConfig.StorageTsdbRetention,
diff --git a/handlers/proxy_logging.go b/handlers/proxy_logging.go
index b68508344..7638488dd 100644
--- a/handlers/proxy_logging.go
+++ b/handlers/proxy_logging.go
@@ -8,11 +8,17 @@ import (
 	"github.com/gorilla/mux"
 
 	"github.com/kiali/kiali/business"
+	"github.com/kiali/kiali/config"
 )
 
 func LoggingUpdate(w http.ResponseWriter, r *http.Request) {
 	params := mux.Vars(r)
 
+	if config.Get().Deployment.ViewOnlyMode {
+		RespondWithError(w, http.StatusForbidden, "Log level cannot be changed in view-only mode")
+		return
+	}
+
 	// Get business layer
 	businessLayer, err := getBusiness(r)
 	if err != nil {
diff --git a/jaeger/client.go b/jaeger/client.go
index 01a771267..83996c5a7 100644
--- a/jaeger/client.go
+++ b/jaeger/client.go
@@ -103,7 +103,7 @@ func NewClient(token string) (*Client, error) {
 		} else {
 			// Legacy HTTP client
 			log.Tracef("Using legacy HTTP client for Jaeger: url=%v, auth.type=%s", u, auth.Type)
-			timeout := time.Duration(5000 * time.Millisecond)
+			timeout := time.Duration(config.Get().ExternalServices.Tracing.QueryTimeout) * time.Second
 			transport, err := httputil.CreateTransport(&auth, &http.Transport{}, timeout, nil)
 			if err != nil {
 				return nil, err
@@ -131,7 +131,7 @@ func (in *Client) GetAppTraces(namespace, app string, q models.TracingQuery) (*J
 			SearchDepth:  int32(q.Limit),
 		},
 	}
-	ctx, cancel := context.WithTimeout(in.ctx, 4*time.Second)
+	ctx, cancel := context.WithTimeout(in.ctx, time.Duration(config.Get().ExternalServices.Tracing.QueryTimeout)*time.Second)
 	defer cancel()
 
 	stream, err := in.grpcClient.FindTraces(ctx, findTracesRQ)
diff --git a/kiali.go b/kiali.go
index b930fb74f..b15172527 100644
--- a/kiali.go
+++ b/kiali.go
@@ -87,11 +87,6 @@ func main() {
 	status.Put(status.CoreCommitHash, commitHash)
 	status.Put(status.ContainerVersion, determineContainerVersion(version))
 
-	// CheckVersionCompatibility check kiali version compatibility with mesh.
-	// The user session is not affected no matter what this check returns, just warning logs.
-	// The complete compatible version matrix is recorded in version-compatibility-matrix.yaml
-	status.CheckVersionCompatibility()
-
 	authentication.InitializeAuthenticationController(cfg.Auth.Strategy)
 
 	// prepare our internal metrics so Prometheus can scrape them
diff --git a/kubernetes/cache/cache.go b/kubernetes/cache/cache.go
index 307837f5c..9ecf43890 100644
--- a/kubernetes/cache/cache.go
+++ b/kubernetes/cache/cache.go
@@ -1,25 +1,12 @@
 package cache
 
 import (
-	"regexp"
-	"strings"
+	"context"
+	"errors"
 	"sync"
 	"time"
 
-	istio "istio.io/client-go/pkg/clientset/versioned"
-	istioext_v1alpha1_listers "istio.io/client-go/pkg/listers/extensions/v1alpha1"
-	istionet_v1alpha3_listers "istio.io/client-go/pkg/listers/networking/v1alpha3"
-	istionet_v1beta1_listers "istio.io/client-go/pkg/listers/networking/v1beta1"
-	istiosec_v1beta1_listers "istio.io/client-go/pkg/listers/security/v1beta1"
-	istiotelem_v1alpha1_listers "istio.io/client-go/pkg/listers/telemetry/v1alpha1"
-	kube "k8s.io/client-go/kubernetes"
-	apps_v1_listers "k8s.io/client-go/listers/apps/v1"
-	core_v1_listers "k8s.io/client-go/listers/core/v1"
-	"k8s.io/client-go/rest"
-	gatewayapi "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
-	k8s_v1alpha2_listers "sigs.k8s.io/gateway-api/pkg/client/listers/apis/v1alpha2"
-
-	kialiConfig "github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
@@ -29,391 +16,157 @@ import (
 // Kiali will use caches for specific namespaces and types
 // https://github.com/istio/istio/blob/master/mixer/adapter/kubernetesenv/cache.go
 
-type (
-	KialiCache interface {
-		// Control methods
-		// Check if a namespace is listed to be cached; if yes, creates a cache for that namespace
-		CheckNamespace(namespace string) bool
-
-		// Refresh will recreate the necessary cache. If the cache is cluster-scoped the "namespace" argument
-		// is ignored and the whole cache is recreated, otherwise only the namespace-specific cache is updated.
-		Refresh(namespace string)
-
-		// Stop all caches
-		Stop()
-
-		// Kubernetes Client used for cache
-		GetClient() *kubernetes.K8SClient
-
-		KubernetesCache
-		IstioCache
-		NamespacesCache
-		ProxyStatusCache
-		RegistryStatusCache
-	}
-
-	namespaceCache struct {
-		created       time.Time
-		namespaces    []models.Namespace
-		nameNamespace map[string]models.Namespace
-	}
-
-	podProxyStatus struct {
-		namespace   string
-		pod         string
-		proxyStatus *kubernetes.ProxyStatus
-	}
-
-	// cacheLister combines a bunch of lister types into one.
-	// This can probably be simplified or turned into an interface
-	// with go generics.
-	cacheLister struct {
-		// Kube listers
-		configMapLister   core_v1_listers.ConfigMapLister
-		daemonSetLister   apps_v1_listers.DaemonSetLister
-		deploymentLister  apps_v1_listers.DeploymentLister
-		endpointLister    core_v1_listers.EndpointsLister
-		podLister         core_v1_listers.PodLister
-		replicaSetLister  apps_v1_listers.ReplicaSetLister
-		serviceLister     core_v1_listers.ServiceLister
-		statefulSetLister apps_v1_listers.StatefulSetLister
+// 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.
+// This business layer cache needs access to all the kiali service account has access
+// to so it uses the kiali service account token instead of a user token. Access to
+// the objects returned by the cache should be filtered/restricted to the user's
+// token access but the cache returns objects without any filtering or restrictions.
+type KialiCache interface {
+	GetKubeCaches() map[string]KubeCache
+
+	// Embedded for backward compatibility for business methods that just use one cluster.
+	// All business methods should eventually use the multi-cluster cache.
+	KubeCache
+	NamespacesCache
+	ProxyStatusCache
+	RegistryStatusCache
+}
 
-		// Istio listers
-		authzLister           istiosec_v1beta1_listers.AuthorizationPolicyLister
-		destinationRuleLister istionet_v1beta1_listers.DestinationRuleLister
-		envoyFilterLister     istionet_v1alpha3_listers.EnvoyFilterLister
-		gatewayLister         istionet_v1beta1_listers.GatewayLister
-		k8sgatewayLister      k8s_v1alpha2_listers.GatewayLister
-		k8shttprouteLister    k8s_v1alpha2_listers.HTTPRouteLister
-		peerAuthnLister       istiosec_v1beta1_listers.PeerAuthenticationLister
-		requestAuthnLister    istiosec_v1beta1_listers.RequestAuthenticationLister
-		serviceEntryLister    istionet_v1beta1_listers.ServiceEntryLister
-		sidecarLister         istionet_v1beta1_listers.SidecarLister
-		telemetryLister       istiotelem_v1alpha1_listers.TelemetryLister
-		virtualServiceLister  istionet_v1beta1_listers.VirtualServiceLister
-		wasmPluginLister      istioext_v1alpha1_listers.WasmPluginLister
-		workloadEntryLister   istionet_v1beta1_listers.WorkloadEntryLister
-		workloadGroupLister   istionet_v1beta1_listers.WorkloadGroupLister
-	}
+// namespaceCache caches namespaces according to their token.
+// TODO: Support multi-cluster.
+type namespaceCache struct {
+	created       time.Time
+	namespaces    []models.Namespace
+	nameNamespace map[string]models.Namespace
+}
 
-	kialiCacheImpl struct {
-		clusterScoped          bool // Creates either cluster-scoped or namespace-scoped informers
-		istioClient            kubernetes.K8SClient
-		k8sApi                 kube.Interface
-		istioApi               istio.Interface
-		gatewayApi             gatewayapi.Interface
-		refreshDuration        time.Duration
-		cacheNamespacesRegexps []regexp.Regexp
-		cacheIstioTypes        map[string]bool
-		stopNSChans            map[string]chan struct{}
-		stopClusterScopedChan  chan struct{} // Close this channel to stop the cluster-scoped informers.
-		cacheLock              sync.RWMutex
-		tokenLock              sync.RWMutex
-		tokenNamespaces        map[string]namespaceCache
-		tokenNamespaceDuration time.Duration
-		proxyStatusLock        sync.RWMutex
-		proxyStatusCreated     *time.Time
-		proxyStatusNamespaces  map[string]map[string]podProxyStatus
-		registryRefreshHandler RegistryRefreshHandler
-		registryStatusLock     sync.RWMutex
-		registryStatusCreated  *time.Time
-		registryStatus         *kubernetes.RegistryStatus
-		// Stops the background goroutine which refreshes the cache's
-		// service account token.
-		stopCacheChan chan bool
+type podProxyStatus struct {
+	namespace   string
+	pod         string
+	proxyStatus *kubernetes.ProxyStatus
+}
 
-		clusterCacheLister *cacheLister
-		nsCacheLister      map[string]*cacheLister
-	}
-)
+type kialiCacheImpl struct {
+	// Embedded for backward compatibility for business methods that just use one cluster.
+	// All business methods should eventually use the multi-cluster cache.
+	KubeCache
+
+	// Stops the background goroutines which refresh the cache's
+	// service account token and poll for istiod's proxy status.
+	cleanup       func()
+	clientFactory kubernetes.ClientFactory
+	// How often the cache will check for kiali SA client changes.
+	clientRefreshPollingPeriod time.Duration
+	// Maps a cluster name to a KubeCache
+	kubeCache              map[string]KubeCache
+	refreshDuration        time.Duration
+	tokenLock              sync.RWMutex
+	tokenNamespaces        map[string]namespaceCache
+	tokenNamespaceDuration time.Duration
+	proxyStatusLock        sync.RWMutex
+	proxyStatusNamespaces  map[string]map[string]podProxyStatus
+	registryStatusLock     sync.RWMutex
+	registryStatusCreated  *time.Time
+	registryStatus         *kubernetes.RegistryStatus
+}
 
-func NewKialiCache(namespaceSeedList ...string) (KialiCache, error) {
-	config, err := kubernetes.ConfigClient()
-	if err != nil {
-		return nil, err
-	}
-	// Kiali Cache will use ServiceAccount token instead of user token
-	// Cache creates watchers that have a long cycle to sync with k8s backend maintaining a cache from events
-	// Cache will be used only for *Get* operations, update/delete operations will executed directly against the API
-	// Cache will see what ServiceAccount can see, so when using OpenShift scenarios, user token is used to fetch the
-	// list of projects/namespaces a specific user can see. When using cache, business layer needs to check if a
-	// specific user can see a specific namespace
-	cacheToken := ""
-	kConfig := kialiConfig.Get()
-	if kConfig.InCluster {
-		if saToken, err := kubernetes.GetKialiToken(); err != nil {
+func NewKialiCache(clientFactory kubernetes.ClientFactory, cfg config.Config, namespaceSeedList ...string) (KialiCache, error) {
+	kialiCacheImpl := kialiCacheImpl{
+		clientFactory:              clientFactory,
+		clientRefreshPollingPeriod: time.Duration(time.Second * 60),
+		kubeCache:                  make(map[string]KubeCache),
+		proxyStatusNamespaces:      make(map[string]map[string]podProxyStatus),
+		refreshDuration:            time.Duration(cfg.KubernetesConfig.CacheDuration) * time.Second,
+		tokenNamespaces:            make(map[string]namespaceCache),
+		tokenNamespaceDuration:     time.Duration(cfg.KubernetesConfig.CacheTokenNamespaceDuration) * time.Second,
+	}
+
+	for cluster, kialiClient := range clientFactory.GetSAClients() {
+		cache, err := NewKubeCache(kialiClient, cfg, NewRegistryHandler(kialiCacheImpl.RefreshRegistryStatus), namespaceSeedList...)
+		if err != nil {
+			log.Errorf("[Kiali Cache] Error creating kube cache for cluster: %s. Err: %v", cluster, err)
 			return nil, err
-		} else {
-			cacheToken = saToken
 		}
+		kialiCacheImpl.kubeCache[cluster] = cache
 	}
 
-	istioConfig := rest.Config{
-		Host:            config.Host,
-		TLSClientConfig: config.TLSClientConfig,
-		QPS:             config.QPS,
-		BearerToken:     cacheToken,
-		Burst:           config.Burst,
+	// TODO: Treat all clusters the same way.
+	homeClient, ok := kialiCacheImpl.kubeCache[kubernetes.HomeClusterName]
+	if !ok {
+		return nil, errors.New("home cluster not configured in kiali cache")
 	}
-	istioClient, err := kubernetes.NewClientFromConfig(&istioConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	refreshDuration := time.Duration(kConfig.KubernetesConfig.CacheDuration) * time.Second
-	tokenNamespaceDuration := time.Duration(kConfig.KubernetesConfig.CacheTokenNamespaceDuration) * time.Second
+	kialiCacheImpl.KubeCache = homeClient
 
-	cacheNamespaces := kConfig.KubernetesConfig.CacheNamespaces
-	cacheIstioTypes := make(map[string]bool)
-	for _, iType := range kConfig.KubernetesConfig.CacheIstioTypes {
-		cacheIstioTypes[iType] = true
+	// Starting background goroutines to:
+	// 1. Refresh the cache's service account token
+	// 2. Poll for istiod's proxy status.
+	// These will stop when the context is cancelled.
+	// Starting goroutines after any errors are handled so as not to leak goroutines.
+	ctx, cancel := context.WithCancel(context.Background())
+	if cfg.ExternalServices.Istio.IstioAPIEnabled {
+		kialiCacheImpl.pollIstiodForProxyStatus(ctx)
 	}
-	log.Tracef("[Kiali Cache] cacheIstioTypes %v", cacheIstioTypes)
 
-	cacheNamespacesRegexps := make([]regexp.Regexp, len(cacheNamespaces))
-	for i, ns := range cacheNamespaces {
-		cacheNamespacesRegexps[i] = *regexp.MustCompile(strings.TrimSpace(ns))
-	}
-
-	kialiCacheImpl := kialiCacheImpl{
-		istioClient:            *istioClient,
-		refreshDuration:        refreshDuration,
-		cacheNamespacesRegexps: cacheNamespacesRegexps,
-		cacheIstioTypes:        cacheIstioTypes,
-		// Only when all namespaces are accessible should the cache be cluster scoped.
-		// Otherwise, kiali may not have access to all namespaces since
-		// the operator only grants clusterroles when all namespaces are accessible.
-		clusterScoped:          kConfig.AllNamespacesAccessible(),
-		tokenNamespaces:        make(map[string]namespaceCache),
-		tokenNamespaceDuration: tokenNamespaceDuration,
-		proxyStatusNamespaces:  make(map[string]map[string]podProxyStatus),
-	}
-	kialiCacheImpl.k8sApi = istioClient.GetK8sApi()
-	kialiCacheImpl.istioApi = istioClient.Istio()
-	kialiCacheImpl.gatewayApi = istioClient.GatewayAPI()
-
-	// Update SA Token
-	kialiCacheImpl.stopCacheChan = kialiCacheImpl.refreshCache(istioConfig)
-	kialiCacheImpl.registryRefreshHandler = NewRegistryHandler(kialiCacheImpl.RefreshRegistryStatus)
+	// Note that this only watches for changes to the home cluster's token since it is
+	// expected that the remote cluster tokens will not change. However, that assumption
+	// may be wrong and in the future the cache may want to watch for changes to all client tokens.
+	kialiCacheImpl.watchForClientChanges(ctx, clientFactory.GetSAHomeClusterClient().GetToken())
 
-	if kialiCacheImpl.clusterScoped {
-		log.Debug("[Kiali Cache] Using 'cluster' scoped Kiali Cache")
-		kialiCacheImpl.createClusterScopedCache()
-	} else {
-		log.Debug("[Kiali Cache] Using 'namespace' scoped Kiali Cache")
-		kialiCacheImpl.nsCacheLister = make(map[string]*cacheLister)
-		kialiCacheImpl.stopNSChans = make(map[string]chan struct{})
+	kialiCacheImpl.cleanup = cancel
 
-		for _, ns := range namespaceSeedList {
-			kialiCacheImpl.CheckNamespace(ns)
-		}
-	}
-
-	log.Infof("Kiali Cache is active for namespaces %v", cacheNamespaces)
 	return &kialiCacheImpl, nil
 }
 
-// It will indicate if a namespace should have a cache
-func (c *kialiCacheImpl) isCached(namespace string) bool {
-	for _, cacheNs := range c.cacheNamespacesRegexps {
-		if cacheNs.MatchString(namespace) {
-			return true
-		}
-	}
-	return false
+// GetKubeCaches returns a kube cache for every configured Kiali Service Account client keyed by cluster name.
+func (c *kialiCacheImpl) GetKubeCaches() map[string]KubeCache {
+	return c.kubeCache
 }
 
-func (c *kialiCacheImpl) createNSCache(namespace string) bool {
-	kubeInformerFactory := c.createKubernetesInformers(namespace)
-	istioInformerFactory := c.createIstioInformers(namespace)
-	gatewayInformerFactory := c.createGatewayInformers(namespace)
-
-	if _, exist := c.stopNSChans[namespace]; !exist {
-		c.stopNSChans[namespace] = make(chan struct{})
-	}
-
-	kubeInformerFactory.Start(c.stopNSChans[namespace])
-	istioInformerFactory.Start(c.stopNSChans[namespace])
-	gatewayInformerFactory.Start(c.stopNSChans[namespace])
-
-	log.Infof("[Kiali Cache] waiting for [namespace: %s] cache to sync", namespace)
-
-	for _, synced := range kubeInformerFactory.WaitForCacheSync(c.stopNSChans[namespace]) {
-		if !synced {
-			log.Errorf("[Kiali Cache] failed to sync [namespace: %s] kube cache", namespace)
-			return false
-		}
-	}
-	for _, synced := range istioInformerFactory.WaitForCacheSync(c.stopNSChans[namespace]) {
-		if !synced {
-			log.Errorf("[Kiali Cache] failed to sync [namespace: %s] Istio cache", namespace)
-			return false
-		}
-	}
-	for _, synced := range gatewayInformerFactory.WaitForCacheSync(c.stopNSChans[namespace]) {
-		if !synced {
-			log.Errorf("[Kiali Cache] failed to sync [namespace: %s] gateway-api cache", namespace)
-			return false
-		}
-	}
-
-	log.Infof("[Kiali Cache] started [namespace: %s] cache", namespace)
-
-	return true
-}
-
-func (c *kialiCacheImpl) createClusterScopedCache() bool {
-	kubeInformerFactory := c.createKubernetesInformers("")
-	istioInformerFactory := c.createIstioInformers("")
-	gatewayInformerFactory := c.createGatewayInformers("")
-
-	c.stopClusterScopedChan = make(chan struct{})
-	kubeInformerFactory.Start(c.stopClusterScopedChan)
-	istioInformerFactory.Start(c.stopClusterScopedChan)
-	gatewayInformerFactory.Start(c.stopClusterScopedChan)
-
-	log.Info("[Kiali Cache] Waiting for cluster-scoped cache to sync")
-	for _, synced := range kubeInformerFactory.WaitForCacheSync(c.stopClusterScopedChan) {
-		if !synced {
-			log.Error("[Kiali Cache] failed to sync kube cluster-scoped cache")
-			return false
-		}
-	}
-	for _, synced := range istioInformerFactory.WaitForCacheSync(c.stopClusterScopedChan) {
-		if !synced {
-			log.Error("[Kiali Cache] failed to sync Istio cluster-scoped cache")
-			return false
-		}
-	}
-	for _, synced := range gatewayInformerFactory.WaitForCacheSync(c.stopClusterScopedChan) {
-		if !synced {
-			log.Error("[Kiali Cache] failed to sync Istio cluster-scoped cache")
-			return false
-		}
-	}
-
-	log.Info("[Kiali Cache] started")
-
-	return true
-}
-
-// CheckNamespace will
-// - Validate if a namespace is included in the cache
-// - Create and initialize a cache
-func (c *kialiCacheImpl) CheckNamespace(namespace string) bool {
-	if c.clusterScoped {
-		return true
-	} else if !c.isCached(namespace) {
-		return false
-	}
-
-	c.cacheLock.RLock()
-	_, isNsCached := c.nsCacheLister[namespace]
-	c.cacheLock.RUnlock()
-
-	if !isNsCached {
-		defer c.cacheLock.Unlock()
-		c.cacheLock.Lock()
-		return c.createNSCache(namespace)
-	}
-
-	return true
-}
-
-// Refresh will recreate the necessary cache. If the cache is cluster-scoped the "namespace" argument
-// is ignored and the whole cache is recreated, otherwise only the namespace-specific cache is updated.
-func (c *kialiCacheImpl) Refresh(namespace string) {
-	defer c.cacheLock.Unlock()
-	c.cacheLock.Lock()
+// Stops all caches across all clusters.
+func (c *kialiCacheImpl) Stop() {
+	log.Infof("Stopping Kiali Cache")
 
-	if c.clusterScoped {
-		c.stop("")
-		c.createClusterScopedCache()
-		return
+	wg := sync.WaitGroup{}
+	for _, kc := range c.kubeCache {
+		wg.Add(1)
+		go func(c KubeCache) {
+			defer wg.Done()
+			c.Stop()
+		}(kc)
 	}
+	wg.Wait()
 
-	c.stop(namespace)
-	c.createNSCache(namespace)
+	c.cleanup()
 }
 
-// refreshCache watches for changes to the cache's service account token
-// and recreates the cache(s) when the token changes.
-func (c *kialiCacheImpl) refreshCache(istioConfig rest.Config) chan bool {
-	ticker := time.NewTicker(60 * time.Second)
-	quit := make(chan bool)
+// watchForClientChanges watches for changes to the cache's service account client
+// and recreates the cache(s) when the client changes. The client is updated when
+// the token for the client changes.
+func (c *kialiCacheImpl) watchForClientChanges(ctx context.Context, token string) {
+	ticker := time.NewTicker(c.clientRefreshPollingPeriod)
 	go func() {
 		for {
 			select {
 			case <-ticker.C:
-				if newToken, err := kubernetes.GetKialiToken(); err != nil {
-					log.Errorf("Error updating Kiali Token %v", err)
-				} else {
-					if istioConfig.BearerToken != newToken {
-						oldToken := istioConfig.BearerToken
-						log.Info("Kiali Cache: Updating cache with new token")
-						istioConfig.BearerToken = newToken
-
-						istioClient, errorInitClient := kubernetes.NewClientFromConfig(&istioConfig)
-						if errorInitClient != nil {
-							log.Errorf("Error creating new Client From Config %v", errorInitClient)
-							istioConfig.BearerToken = oldToken
-						} else {
-							c.istioClient = *istioClient
-							c.k8sApi = istioClient.GetK8sApi()
-							c.istioApi = istioClient.Istio()
-							c.gatewayApi = istioClient.GatewayAPI()
-							if c.clusterScoped {
-								c.Refresh("")
-							} else {
-								// Need to recreate all the namespace scoped caches
-								// with the new token when it changes.
-								for ns := range c.stopNSChans {
-									c.Refresh(ns)
-								}
-							}
-						}
+				if c.clientFactory.GetSAHomeClusterClient().GetToken() != token {
+					log.Info("[Kiali Cache] Updating cache with new token")
 
-					} else {
-						log.Debug("Kiali Cache: Nothing to refresh")
+					if err := c.KubeCache.UpdateClient(c.clientFactory.GetSAHomeClusterClient()); err != nil {
+						log.Errorf("[Kiali Cache] Error updating cache with new token. Err: %s", err)
+						// Try again on the next tick without updating the token.
+						continue
 					}
+
+					token = c.clientFactory.GetSAHomeClusterClient().GetToken()
+				} else {
+					log.Debug("[Kiali Cache] Nothing to refresh")
 				}
-			case <-quit:
+			case <-ctx.Done():
+				log.Debug("[Kiali Cache] Stopping watching for service account token changes")
 				ticker.Stop()
 				return
 			}
 		}
 	}()
-
-	return quit
-}
-
-// Stop will stop either the cluster wide cache or all of the namespace caches.
-func (c *kialiCacheImpl) Stop() {
-	log.Infof("Stopping Kiali Cache")
-	defer c.cacheLock.Unlock()
-	c.cacheLock.Lock()
-	if c.clusterScoped {
-		c.stop("")
-	} else {
-		for namespace := range c.stopNSChans {
-			c.stop(namespace)
-		}
-	}
-	close(c.stopCacheChan)
-}
-
-func (c *kialiCacheImpl) stop(namespace string) {
-	if c.clusterScoped {
-		close(c.stopClusterScopedChan)
-	} else {
-		if nsChan, exist := c.stopNSChans[namespace]; exist {
-			close(nsChan)
-			delete(c.stopNSChans, namespace)
-			delete(c.nsCacheLister, namespace)
-		}
-	}
-}
-
-func (c *kialiCacheImpl) GetClient() *kubernetes.K8SClient {
-	return &c.istioClient
 }
diff --git a/kubernetes/cache/cachingclient.go b/kubernetes/cache/cachingclient.go
new file mode 100644
index 000000000..c45ed35c8
--- /dev/null
+++ b/kubernetes/cache/cachingclient.go
@@ -0,0 +1,111 @@
+package cache
+
+import (
+	apps_v1 "k8s.io/api/apps/v1"
+	core_v1 "k8s.io/api/core/v1"
+
+	"github.com/kiali/kiali/kubernetes"
+)
+
+// CachingClient is a wrapper around a ClientInterface that adds caching.
+// If the object is cached then it is read from the cache. Not all object
+// types are cached. When an object is created, updated, or deleted, then
+// the cache is refreshed and updated.
+type CachingClient struct {
+	cache KialiCache
+	kubernetes.ClientInterface
+}
+
+// NewCachingClient creates a new CachingClient out of a cache and a client.
+func NewCachingClient(cache KialiCache, client kubernetes.ClientInterface) *CachingClient {
+	return &CachingClient{
+		cache:           cache,
+		ClientInterface: client,
+	}
+}
+
+// TODO: Remove conditionals for cache once cache is fully mandatory.
+
+func (cc *CachingClient) GetConfigMap(namespace, name string) (*core_v1.ConfigMap, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetConfigMap(namespace, name)
+	}
+	return cc.ClientInterface.GetConfigMap(namespace, name)
+}
+
+func (cc *CachingClient) GetDaemonSet(namespace string, name string) (*apps_v1.DaemonSet, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetDaemonSet(namespace, name)
+	}
+	return cc.ClientInterface.GetDaemonSet(namespace, name)
+}
+
+func (cc *CachingClient) GetDaemonSets(namespace string) ([]apps_v1.DaemonSet, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetDaemonSets(namespace)
+	}
+	return cc.ClientInterface.GetDaemonSets(namespace)
+}
+
+func (cc *CachingClient) GetDeployment(namespace string, name string) (*apps_v1.Deployment, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetDeployment(namespace, name)
+	}
+	return cc.ClientInterface.GetDeployment(namespace, name)
+}
+
+func (cc *CachingClient) GetDeployments(namespace string) ([]apps_v1.Deployment, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetDeployments(namespace)
+	}
+	return cc.ClientInterface.GetDeployments(namespace)
+}
+
+func (cc *CachingClient) GetEndpoints(namespace string, name string) (*core_v1.Endpoints, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetEndpoints(namespace, name)
+	}
+	return cc.ClientInterface.GetEndpoints(namespace, name)
+}
+
+func (cc *CachingClient) GetPods(namespace, labelSelector string) ([]core_v1.Pod, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetPods(namespace, labelSelector)
+	}
+	return cc.ClientInterface.GetPods(namespace, labelSelector)
+}
+
+func (cc *CachingClient) GetReplicaSets(namespace string) ([]apps_v1.ReplicaSet, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetReplicaSets(namespace)
+	}
+	return cc.ClientInterface.GetReplicaSets(namespace)
+}
+
+func (cc *CachingClient) GetService(namespace string, name string) (*core_v1.Service, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetService(namespace, name)
+	}
+	return cc.ClientInterface.GetService(namespace, name)
+}
+
+func (cc *CachingClient) GetServices(namespace string, selectorLabels map[string]string) ([]core_v1.Service, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetServices(namespace, selectorLabels)
+	}
+	return cc.ClientInterface.GetServices(namespace, selectorLabels)
+}
+
+func (cc *CachingClient) GetStatefulSet(namespace string, name string) (*apps_v1.StatefulSet, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetStatefulSet(namespace, name)
+	}
+	return cc.ClientInterface.GetStatefulSet(namespace, name)
+}
+
+func (cc *CachingClient) GetStatefulSets(namespace string) ([]apps_v1.StatefulSet, error) {
+	if cc.cache.CheckNamespace(namespace) {
+		return cc.cache.GetStatefulSets(namespace)
+	}
+	return cc.ClientInterface.GetStatefulSets(namespace)
+}
diff --git a/kubernetes/cache/istio.go b/kubernetes/cache/istio.go
deleted file mode 100644
index 553a28552..000000000
--- a/kubernetes/cache/istio.go
+++ /dev/null
@@ -1,752 +0,0 @@
-package cache
-
-import (
-	"fmt"
-
-	extentions_v1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1"
-	networking_v1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
-	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
-	security_v1beta1 "istio.io/client-go/pkg/apis/security/v1beta1"
-	"istio.io/client-go/pkg/apis/telemetry/v1alpha1"
-	istio "istio.io/client-go/pkg/informers/externalversions"
-	"k8s.io/apimachinery/pkg/labels"
-	gatewayapi "sigs.k8s.io/gateway-api/apis/v1alpha2"
-	gateway "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions"
-
-	"github.com/kiali/kiali/kubernetes"
-)
-
-type (
-	IstioCache interface {
-		CheckIstioResource(resourceType string) bool
-
-		GetDestinationRule(namespace, name string) (*networking_v1beta1.DestinationRule, error)
-		GetDestinationRules(namespace, labelSelector string) ([]*networking_v1beta1.DestinationRule, error)
-		GetEnvoyFilter(namespace, name string) (*networking_v1alpha3.EnvoyFilter, error)
-		GetEnvoyFilters(namespace, labelSelector string) ([]*networking_v1alpha3.EnvoyFilter, error)
-		GetGateway(namespace, name string) (*networking_v1beta1.Gateway, error)
-		GetGateways(namespace, labelSelector string) ([]*networking_v1beta1.Gateway, error)
-		GetServiceEntry(namespace, name string) (*networking_v1beta1.ServiceEntry, error)
-		GetServiceEntries(namespace, labelSelector string) ([]*networking_v1beta1.ServiceEntry, error)
-		GetSidecar(namespace, name string) (*networking_v1beta1.Sidecar, error)
-		GetSidecars(namespace, labelSelector string) ([]*networking_v1beta1.Sidecar, error)
-		GetVirtualService(namespace, name string) (*networking_v1beta1.VirtualService, error)
-		GetVirtualServices(namespace, labelSelector string) ([]*networking_v1beta1.VirtualService, error)
-		GetWorkloadEntry(namespace, name string) (*networking_v1beta1.WorkloadEntry, error)
-		GetWorkloadEntries(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadEntry, error)
-		GetWorkloadGroup(namespace, name string) (*networking_v1beta1.WorkloadGroup, error)
-		GetWorkloadGroups(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadGroup, error)
-		GetWasmPlugin(namespace, name string) (*extentions_v1alpha1.WasmPlugin, error)
-		GetWasmPlugins(namespace, labelSelector string) ([]*extentions_v1alpha1.WasmPlugin, error)
-		GetTelemetry(namespace, name string) (*v1alpha1.Telemetry, error)
-		GetTelemetries(namespace, labelSelector string) ([]*v1alpha1.Telemetry, error)
-
-		GetK8sGateway(namespace, name string) (*gatewayapi.Gateway, error)
-		GetK8sGateways(namespace, labelSelector string) ([]*gatewayapi.Gateway, error)
-		GetK8sHTTPRoute(namespace, name string) (*gatewayapi.HTTPRoute, error)
-		GetK8sHTTPRoutes(namespace, labelSelector string) ([]*gatewayapi.HTTPRoute, error)
-
-		GetAuthorizationPolicy(namespace, name string) (*security_v1beta1.AuthorizationPolicy, error)
-		GetAuthorizationPolicies(namespace, labelSelector string) ([]*security_v1beta1.AuthorizationPolicy, error)
-		GetPeerAuthentication(namespace, name string) (*security_v1beta1.PeerAuthentication, error)
-		GetPeerAuthentications(namespace, labelSelector string) ([]*security_v1beta1.PeerAuthentication, error)
-		GetRequestAuthentication(namespace, name string) (*security_v1beta1.RequestAuthentication, error)
-		GetRequestAuthentications(namespace, labelSelector string) ([]*security_v1beta1.RequestAuthentication, error)
-	}
-)
-
-func (c *kialiCacheImpl) CheckIstioResource(resourceType string) bool {
-	// cacheIstioTypes stores the single types but for compatibility with kubernetes api resourceType will use plurals
-	_, exist := c.cacheIstioTypes[kubernetes.PluralType[resourceType]]
-	return exist
-}
-
-func (c *kialiCacheImpl) createIstioInformers(namespace string) istio.SharedInformerFactory {
-	var opts []istio.SharedInformerOption
-	if namespace != "" {
-		opts = append(opts, istio.WithNamespace(namespace))
-	}
-	sharedInformers := istio.NewSharedInformerFactoryWithOptions(c.istioApi, c.refreshDuration, opts...)
-	lister := c.getCacheLister(namespace)
-
-	if c.CheckIstioResource(kubernetes.AuthorizationPolicies) {
-		lister.authzLister = sharedInformers.Security().V1beta1().AuthorizationPolicies().Lister()
-		sharedInformers.Security().V1beta1().AuthorizationPolicies().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.DestinationRules) {
-		lister.destinationRuleLister = sharedInformers.Networking().V1beta1().DestinationRules().Lister()
-		sharedInformers.Networking().V1beta1().DestinationRules().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.EnvoyFilters) {
-		lister.envoyFilterLister = sharedInformers.Networking().V1alpha3().EnvoyFilters().Lister()
-		sharedInformers.Networking().V1alpha3().EnvoyFilters().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.Gateways) {
-		lister.gatewayLister = sharedInformers.Networking().V1beta1().Gateways().Lister()
-		sharedInformers.Networking().V1beta1().Gateways().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.PeerAuthentications) {
-		lister.peerAuthnLister = sharedInformers.Security().V1beta1().PeerAuthentications().Lister()
-		sharedInformers.Security().V1beta1().PeerAuthentications().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.RequestAuthentications) {
-		lister.requestAuthnLister = sharedInformers.Security().V1beta1().RequestAuthentications().Lister()
-		sharedInformers.Security().V1beta1().RequestAuthentications().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.ServiceEntries) {
-		lister.serviceEntryLister = sharedInformers.Networking().V1beta1().ServiceEntries().Lister()
-		sharedInformers.Networking().V1beta1().ServiceEntries().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.Sidecars) {
-		lister.sidecarLister = sharedInformers.Networking().V1beta1().Sidecars().Lister()
-		sharedInformers.Networking().V1beta1().Sidecars().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.Telemetries) {
-		lister.telemetryLister = sharedInformers.Telemetry().V1alpha1().Telemetries().Lister()
-		sharedInformers.Telemetry().V1alpha1().Telemetries().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.VirtualServices) {
-		lister.virtualServiceLister = sharedInformers.Networking().V1beta1().VirtualServices().Lister()
-		sharedInformers.Networking().V1beta1().VirtualServices().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.WasmPlugins) {
-		lister.wasmPluginLister = sharedInformers.Extensions().V1alpha1().WasmPlugins().Lister()
-		sharedInformers.Extensions().V1alpha1().WasmPlugins().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.WorkloadEntries) {
-		lister.workloadEntryLister = sharedInformers.Networking().V1beta1().WorkloadEntries().Lister()
-		sharedInformers.Networking().V1beta1().WorkloadEntries().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-	if c.CheckIstioResource(kubernetes.WorkloadGroups) {
-		lister.workloadGroupLister = sharedInformers.Networking().V1beta1().WorkloadGroups().Lister()
-		sharedInformers.Networking().V1beta1().WorkloadGroups().Informer().AddEventHandler(c.registryRefreshHandler)
-	}
-
-	return sharedInformers
-}
-
-func (c *kialiCacheImpl) createGatewayInformers(namespace string) gateway.SharedInformerFactory {
-	sharedInformers := gateway.NewSharedInformerFactory(c.gatewayApi, c.refreshDuration)
-	lister := c.getCacheLister(namespace)
-
-	if c.istioClient.IsGatewayAPI() {
-		if c.CheckIstioResource(kubernetes.K8sGateways) {
-			lister.k8sgatewayLister = sharedInformers.Gateway().V1alpha2().Gateways().Lister()
-			sharedInformers.Gateway().V1alpha2().Gateways().Informer().AddEventHandler(c.registryRefreshHandler)
-		}
-		if c.CheckIstioResource(kubernetes.K8sHTTPRoutes) {
-			lister.k8shttprouteLister = sharedInformers.Gateway().V1alpha2().HTTPRoutes().Lister()
-			sharedInformers.Gateway().V1alpha2().Gateways().Informer().AddEventHandler(c.registryRefreshHandler)
-		}
-	}
-	return sharedInformers
-}
-
-func (c *kialiCacheImpl) GetDestinationRule(namespace, name string) (*networking_v1beta1.DestinationRule, error) {
-	if !c.CheckIstioResource(kubernetes.DestinationRules) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.DestinationRuleType)
-	}
-
-	dr, err := c.getCacheLister(namespace).destinationRuleLister.DestinationRules(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	dr.Kind = kubernetes.DestinationRuleType
-	return dr, nil
-}
-
-func (c *kialiCacheImpl) GetDestinationRules(namespace, labelSelector string) ([]*networking_v1beta1.DestinationRule, error) {
-	if !c.CheckIstioResource(kubernetes.DestinationRules) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.DestinationRuleType)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	drs, err := c.getCacheLister(namespace).destinationRuleLister.DestinationRules(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if drs == nil {
-		return []*networking_v1beta1.DestinationRule{}, nil
-	}
-
-	for _, dr := range drs {
-		dr.Kind = kubernetes.DestinationRuleType
-	}
-	return drs, nil
-}
-
-func (c *kialiCacheImpl) GetEnvoyFilter(namespace, name string) (*networking_v1alpha3.EnvoyFilter, error) {
-	if !c.CheckIstioResource(kubernetes.EnvoyFilters) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.EnvoyFilterType)
-	}
-
-	ef, err := c.getCacheLister(namespace).envoyFilterLister.EnvoyFilters(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	ef.Kind = kubernetes.EnvoyFilterType
-	return ef, nil
-}
-
-func (c *kialiCacheImpl) GetEnvoyFilters(namespace, labelSelector string) ([]*networking_v1alpha3.EnvoyFilter, error) {
-	if !c.CheckIstioResource(kubernetes.EnvoyFilters) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.EnvoyFilterType)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	efs, err := c.getCacheLister(namespace).envoyFilterLister.EnvoyFilters(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if efs == nil {
-		return []*networking_v1alpha3.EnvoyFilter{}, nil
-	}
-
-	for _, ef := range efs {
-		ef.Kind = kubernetes.EnvoyFilterType
-	}
-	return efs, nil
-}
-
-func (c *kialiCacheImpl) GetGateway(namespace, name string) (*networking_v1beta1.Gateway, error) {
-	if !c.CheckIstioResource(kubernetes.Gateways) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.GatewayType)
-	}
-
-	gw, err := c.getCacheLister(namespace).gatewayLister.Gateways(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	gw.Kind = kubernetes.GatewayType
-	return gw, nil
-}
-
-func (c *kialiCacheImpl) GetGateways(namespace, labelSelector string) ([]*networking_v1beta1.Gateway, error) {
-	if !c.CheckIstioResource(kubernetes.Gateways) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.Gateways)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	gateways, err := c.getCacheLister(namespace).gatewayLister.Gateways(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if gateways == nil {
-		return []*networking_v1beta1.Gateway{}, nil
-	}
-
-	for _, gw := range gateways {
-		gw.Kind = kubernetes.GatewayType
-	}
-	return gateways, nil
-}
-
-func (c *kialiCacheImpl) GetServiceEntry(namespace, name string) (*networking_v1beta1.ServiceEntry, error) {
-	if !c.CheckIstioResource(kubernetes.ServiceEntries) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.ServiceEntryType)
-	}
-
-	se, err := c.getCacheLister(namespace).serviceEntryLister.ServiceEntries(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	se.Kind = kubernetes.ServiceEntryType
-	return se, nil
-}
-
-func (c *kialiCacheImpl) GetServiceEntries(namespace, labelSelector string) ([]*networking_v1beta1.ServiceEntry, error) {
-	if !c.CheckIstioResource(kubernetes.ServiceEntries) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.ServiceEntryType)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	ses, err := c.getCacheLister(namespace).serviceEntryLister.ServiceEntries(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if ses == nil {
-		return []*networking_v1beta1.ServiceEntry{}, nil
-	}
-
-	for _, se := range ses {
-		se.Kind = kubernetes.ServiceEntryType
-	}
-	return ses, nil
-}
-
-func (c *kialiCacheImpl) GetSidecar(namespace, name string) (*networking_v1beta1.Sidecar, error) {
-	if !c.CheckIstioResource(kubernetes.Sidecars) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.SidecarType)
-	}
-
-	sc, err := c.getCacheLister(namespace).sidecarLister.Sidecars(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	sc.Kind = kubernetes.SidecarType
-	return sc, nil
-}
-
-func (c *kialiCacheImpl) GetSidecars(namespace, labelSelector string) ([]*networking_v1beta1.Sidecar, error) {
-	if !c.CheckIstioResource(kubernetes.Sidecars) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.SidecarType)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	sidecars, err := c.getCacheLister(namespace).sidecarLister.Sidecars(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if sidecars == nil {
-		return []*networking_v1beta1.Sidecar{}, nil
-	}
-
-	for _, sc := range sidecars {
-		sc.Kind = kubernetes.SidecarType
-	}
-	return sidecars, nil
-}
-
-func (c *kialiCacheImpl) GetVirtualService(namespace, name string) (*networking_v1beta1.VirtualService, error) {
-	if !c.CheckIstioResource(kubernetes.VirtualServices) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.VirtualServiceType)
-	}
-
-	vs, err := c.getCacheLister(namespace).virtualServiceLister.VirtualServices(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	vs.Kind = kubernetes.VirtualServiceType
-	return vs, nil
-}
-
-func (c *kialiCacheImpl) GetVirtualServices(namespace, labelSelector string) ([]*networking_v1beta1.VirtualService, error) {
-	if !c.CheckIstioResource(kubernetes.VirtualServices) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.VirtualServiceType)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	vs, err := c.getCacheLister(namespace).virtualServiceLister.VirtualServices(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if vs == nil {
-		return []*networking_v1beta1.VirtualService{}, nil
-	}
-
-	for _, v := range vs {
-		v.Kind = kubernetes.VirtualServiceType
-	}
-	return vs, nil
-}
-
-func (c *kialiCacheImpl) GetWorkloadEntry(namespace, name string) (*networking_v1beta1.WorkloadEntry, error) {
-	if !c.CheckIstioResource(kubernetes.WorkloadEntries) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadEntryType)
-	}
-
-	we, err := c.getCacheLister(namespace).workloadEntryLister.WorkloadEntries(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	we.Kind = kubernetes.WorkloadEntryType
-	return we, nil
-}
-
-func (c *kialiCacheImpl) GetWorkloadEntries(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadEntry, error) {
-	if !c.CheckIstioResource(kubernetes.WorkloadEntries) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadEntryType)
-	}
-
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	we, err := c.getCacheLister(namespace).workloadEntryLister.WorkloadEntries(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if we == nil {
-		return []*networking_v1beta1.WorkloadEntry{}, nil
-	}
-
-	for _, w := range we {
-		w.Kind = kubernetes.WorkloadEntryType
-	}
-	return we, nil
-}
-
-func (c *kialiCacheImpl) GetWorkloadGroup(namespace, name string) (*networking_v1beta1.WorkloadGroup, error) {
-	if !c.CheckIstioResource(kubernetes.WorkloadGroups) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadGroupType)
-	}
-
-	wg, err := c.getCacheLister(namespace).workloadGroupLister.WorkloadGroups(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	wg.Kind = kubernetes.WorkloadGroupType
-	return wg, nil
-}
-
-func (c *kialiCacheImpl) GetWorkloadGroups(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadGroup, error) {
-	if !c.CheckIstioResource(kubernetes.WorkloadGroups) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadGroups)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	wg, err := c.getCacheLister(namespace).workloadGroupLister.WorkloadGroups(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if wg == nil {
-		return []*networking_v1beta1.WorkloadGroup{}, nil
-	}
-
-	for _, w := range wg {
-		w.Kind = kubernetes.WorkloadGroupType
-	}
-	return wg, nil
-}
-
-func (c *kialiCacheImpl) GetWasmPlugin(namespace, name string) (*extentions_v1alpha1.WasmPlugin, error) {
-	if !c.CheckIstioResource(kubernetes.WasmPlugins) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WasmPluginType)
-	}
-
-	wp, err := c.getCacheLister(namespace).wasmPluginLister.WasmPlugins(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	wp.Kind = kubernetes.WasmPluginType
-	return wp, nil
-}
-
-func (c *kialiCacheImpl) GetWasmPlugins(namespace, labelSelector string) ([]*extentions_v1alpha1.WasmPlugin, error) {
-	if !c.CheckIstioResource(kubernetes.WasmPlugins) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WasmPlugins)
-	}
-
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	wp, err := c.getCacheLister(namespace).wasmPluginLister.WasmPlugins(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if wp == nil {
-		return []*extentions_v1alpha1.WasmPlugin{}, nil
-	}
-
-	for _, w := range wp {
-		w.Kind = kubernetes.WasmPluginType
-	}
-	return wp, nil
-}
-
-func (c *kialiCacheImpl) GetTelemetry(namespace, name string) (*v1alpha1.Telemetry, error) {
-	if !c.CheckIstioResource(kubernetes.Telemetries) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.TelemetryType)
-	}
-
-	t, err := c.getCacheLister(namespace).telemetryLister.Telemetries(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	t.Kind = kubernetes.TelemetryType
-	return t, nil
-}
-
-func (c *kialiCacheImpl) GetTelemetries(namespace, labelSelector string) ([]*v1alpha1.Telemetry, error) {
-	if !c.CheckIstioResource(kubernetes.Telemetries) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.Telemetries)
-	}
-
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	t, err := c.getCacheLister(namespace).telemetryLister.Telemetries(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if t == nil {
-		return []*v1alpha1.Telemetry{}, nil
-	}
-
-	for _, w := range t {
-		w.Kind = kubernetes.TelemetryType
-	}
-
-	return t, nil
-}
-
-func (c *kialiCacheImpl) GetK8sGateway(namespace, name string) (*gatewayapi.Gateway, error) {
-	if !c.CheckIstioResource(kubernetes.K8sGateways) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sGatewayType)
-	}
-
-	g, err := c.getCacheLister(namespace).k8sgatewayLister.Gateways(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	g.Kind = kubernetes.K8sGatewayType
-	return g, nil
-}
-
-func (c *kialiCacheImpl) GetK8sGateways(namespace, labelSelector string) ([]*gatewayapi.Gateway, error) {
-	if !c.CheckIstioResource(kubernetes.K8sGateways) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sGateways)
-	}
-
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	g, err := c.getCacheLister(namespace).k8sgatewayLister.Gateways(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if g == nil {
-		return []*gatewayapi.Gateway{}, nil
-	}
-
-	for _, w := range g {
-		w.Kind = kubernetes.K8sGatewayType
-	}
-
-	return g, nil
-}
-
-func (c *kialiCacheImpl) GetK8sHTTPRoute(namespace, name string) (*gatewayapi.HTTPRoute, error) {
-	if !c.CheckIstioResource(kubernetes.K8sHTTPRoutes) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sHTTPRouteType)
-	}
-
-	g, err := c.getCacheLister(namespace).k8shttprouteLister.HTTPRoutes(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	g.Kind = kubernetes.K8sHTTPRouteType
-	return g, nil
-}
-
-func (c *kialiCacheImpl) GetK8sHTTPRoutes(namespace, labelSelector string) ([]*gatewayapi.HTTPRoute, error) {
-	if !c.CheckIstioResource(kubernetes.K8sHTTPRoutes) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sHTTPRoutes)
-	}
-
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	r, err := c.getCacheLister(namespace).k8shttprouteLister.HTTPRoutes(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if r == nil {
-		return []*gatewayapi.HTTPRoute{}, nil
-	}
-
-	for _, w := range r {
-		w.Kind = kubernetes.K8sHTTPRouteType
-	}
-
-	return r, nil
-}
-
-func (c *kialiCacheImpl) GetAuthorizationPolicy(namespace, name string) (*security_v1beta1.AuthorizationPolicy, error) {
-	if !c.CheckIstioResource(kubernetes.AuthorizationPolicies) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.AuthorizationPoliciesType)
-	}
-
-	ap, err := c.getCacheLister(namespace).authzLister.AuthorizationPolicies(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	ap.Kind = kubernetes.AuthorizationPoliciesType
-	return ap, nil
-}
-
-func (c *kialiCacheImpl) GetAuthorizationPolicies(namespace, labelSelector string) ([]*security_v1beta1.AuthorizationPolicy, error) {
-	if !c.CheckIstioResource(kubernetes.AuthorizationPolicies) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.AuthorizationPolicies)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	authPolicies, err := c.getCacheLister(namespace).authzLister.AuthorizationPolicies(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if authPolicies == nil {
-		return []*security_v1beta1.AuthorizationPolicy{}, nil
-	}
-
-	for _, ap := range authPolicies {
-		ap.Kind = kubernetes.AuthorizationPoliciesType
-	}
-	return authPolicies, nil
-}
-
-func (c *kialiCacheImpl) GetPeerAuthentication(namespace, name string) (*security_v1beta1.PeerAuthentication, error) {
-	if !c.CheckIstioResource(kubernetes.PeerAuthentications) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.PeerAuthenticationsType)
-	}
-
-	pa, err := c.getCacheLister(namespace).peerAuthnLister.PeerAuthentications(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	pa.Kind = kubernetes.PeerAuthenticationsType
-	return pa, nil
-}
-
-func (c *kialiCacheImpl) GetPeerAuthentications(namespace, labelSelector string) ([]*security_v1beta1.PeerAuthentication, error) {
-	if !c.CheckIstioResource(kubernetes.PeerAuthentications) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.PeerAuthenticationsType)
-	}
-
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	peerAuths, err := c.getCacheLister(namespace).peerAuthnLister.PeerAuthentications(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if peerAuths == nil {
-		return []*security_v1beta1.PeerAuthentication{}, nil
-	}
-
-	for _, pa := range peerAuths {
-		pa.Kind = kubernetes.PeerAuthenticationsType
-	}
-	return peerAuths, nil
-}
-
-func (c *kialiCacheImpl) GetRequestAuthentication(namespace, name string) (*security_v1beta1.RequestAuthentication, error) {
-	if !c.CheckIstioResource(kubernetes.RequestAuthentications) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.RequestAuthentications)
-	}
-
-	ra, err := c.getCacheLister(namespace).requestAuthnLister.RequestAuthentications(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	ra.Kind = kubernetes.RequestAuthenticationsType
-	return ra, nil
-}
-
-func (c *kialiCacheImpl) GetRequestAuthentications(namespace, labelSelector string) ([]*security_v1beta1.RequestAuthentication, error) {
-	if !c.CheckIstioResource(kubernetes.RequestAuthentications) {
-		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.RequestAuthenticationsType)
-	}
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	reqAuths, err := c.getCacheLister(namespace).requestAuthnLister.RequestAuthentications(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-
-	// Lister returns nil when there are no results but callers of the cache expect an empty array
-	// so keeping the behavior the same since it matters for json marshalling.
-	if reqAuths == nil {
-		return []*security_v1beta1.RequestAuthentication{}, nil
-	}
-
-	for _, ra := range reqAuths {
-		ra.Kind = kubernetes.RequestAuthenticationsType
-	}
-	return reqAuths, nil
-}
diff --git a/kubernetes/cache/kube_cache.go b/kubernetes/cache/kube_cache.go
new file mode 100644
index 000000000..2b5d65e9f
--- /dev/null
+++ b/kubernetes/cache/kube_cache.go
@@ -0,0 +1,1563 @@
+package cache
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+	"sync"
+	"time"
+
+	extentions_v1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1"
+	networking_v1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
+	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	security_v1beta1 "istio.io/client-go/pkg/apis/security/v1beta1"
+	"istio.io/client-go/pkg/apis/telemetry/v1alpha1"
+	istio "istio.io/client-go/pkg/informers/externalversions"
+	istioext_v1alpha1_listers "istio.io/client-go/pkg/listers/extensions/v1alpha1"
+	istionet_v1alpha3_listers "istio.io/client-go/pkg/listers/networking/v1alpha3"
+	istionet_v1beta1_listers "istio.io/client-go/pkg/listers/networking/v1beta1"
+	istiosec_v1beta1_listers "istio.io/client-go/pkg/listers/security/v1beta1"
+	istiotelem_v1alpha1_listers "istio.io/client-go/pkg/listers/telemetry/v1alpha1"
+	apps_v1 "k8s.io/api/apps/v1"
+	core_v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/informers"
+	apps_v1_listers "k8s.io/client-go/listers/apps/v1"
+	core_v1_listers "k8s.io/client-go/listers/core/v1"
+	"k8s.io/client-go/tools/cache"
+	gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+	gateway "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions"
+	k8s_v1beta1_listers "sigs.k8s.io/gateway-api/pkg/client/listers/apis/v1beta1"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/log"
+)
+
+type KubeCache interface {
+	// Control methods
+	// Check if a namespace is listed to be cached; if yes, creates a cache for that namespace
+	CheckNamespace(namespace string) bool
+
+	// Refresh will recreate the necessary cache. If the cache is cluster-scoped the "namespace" argument
+	// is ignored and the whole cache is recreated, otherwise only the namespace-specific cache is updated.
+	Refresh(namespace string)
+
+	// Stop all caches
+	Stop()
+
+	// UpdateClient will update the client used by the cache.
+	// Useful for when the token is refreshed for the client.
+	// This causes a full refresh of the cache.
+	UpdateClient(client kubernetes.ClientInterface) error
+
+	CheckIstioResource(resourceType string) bool
+
+	GetConfigMap(namespace, name string) (*core_v1.ConfigMap, error)
+	GetDaemonSets(namespace string) ([]apps_v1.DaemonSet, error)
+	GetDaemonSet(namespace, name string) (*apps_v1.DaemonSet, error)
+	GetDeployments(namespace string) ([]apps_v1.Deployment, error)
+	GetDeployment(namespace, name string) (*apps_v1.Deployment, error)
+	GetEndpoints(namespace, name string) (*core_v1.Endpoints, error)
+	GetStatefulSets(namespace string) ([]apps_v1.StatefulSet, error)
+	GetStatefulSet(namespace, name string) (*apps_v1.StatefulSet, error)
+	GetServices(namespace string, selectorLabels map[string]string) ([]core_v1.Service, error)
+	GetService(namespace string, name string) (*core_v1.Service, error)
+	GetPods(namespace, labelSelector string) ([]core_v1.Pod, error)
+	GetReplicaSets(namespace string) ([]apps_v1.ReplicaSet, error)
+
+	GetDestinationRule(namespace, name string) (*networking_v1beta1.DestinationRule, error)
+	GetDestinationRules(namespace, labelSelector string) ([]*networking_v1beta1.DestinationRule, error)
+	GetEnvoyFilter(namespace, name string) (*networking_v1alpha3.EnvoyFilter, error)
+	GetEnvoyFilters(namespace, labelSelector string) ([]*networking_v1alpha3.EnvoyFilter, error)
+	GetGateway(namespace, name string) (*networking_v1beta1.Gateway, error)
+	GetGateways(namespace, labelSelector string) ([]*networking_v1beta1.Gateway, error)
+	GetServiceEntry(namespace, name string) (*networking_v1beta1.ServiceEntry, error)
+	GetServiceEntries(namespace, labelSelector string) ([]*networking_v1beta1.ServiceEntry, error)
+	GetSidecar(namespace, name string) (*networking_v1beta1.Sidecar, error)
+	GetSidecars(namespace, labelSelector string) ([]*networking_v1beta1.Sidecar, error)
+	GetVirtualService(namespace, name string) (*networking_v1beta1.VirtualService, error)
+	GetVirtualServices(namespace, labelSelector string) ([]*networking_v1beta1.VirtualService, error)
+	GetWorkloadEntry(namespace, name string) (*networking_v1beta1.WorkloadEntry, error)
+	GetWorkloadEntries(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadEntry, error)
+	GetWorkloadGroup(namespace, name string) (*networking_v1beta1.WorkloadGroup, error)
+	GetWorkloadGroups(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadGroup, error)
+	GetWasmPlugin(namespace, name string) (*extentions_v1alpha1.WasmPlugin, error)
+	GetWasmPlugins(namespace, labelSelector string) ([]*extentions_v1alpha1.WasmPlugin, error)
+	GetTelemetry(namespace, name string) (*v1alpha1.Telemetry, error)
+	GetTelemetries(namespace, labelSelector string) ([]*v1alpha1.Telemetry, error)
+
+	GetK8sGateway(namespace, name string) (*gatewayapi_v1beta1.Gateway, error)
+	GetK8sGateways(namespace, labelSelector string) ([]*gatewayapi_v1beta1.Gateway, error)
+	GetK8sHTTPRoute(namespace, name string) (*gatewayapi_v1beta1.HTTPRoute, error)
+	GetK8sHTTPRoutes(namespace, labelSelector string) ([]*gatewayapi_v1beta1.HTTPRoute, error)
+
+	GetAuthorizationPolicy(namespace, name string) (*security_v1beta1.AuthorizationPolicy, error)
+	GetAuthorizationPolicies(namespace, labelSelector string) ([]*security_v1beta1.AuthorizationPolicy, error)
+	GetPeerAuthentication(namespace, name string) (*security_v1beta1.PeerAuthentication, error)
+	GetPeerAuthentications(namespace, labelSelector string) ([]*security_v1beta1.PeerAuthentication, error)
+	GetRequestAuthentication(namespace, name string) (*security_v1beta1.RequestAuthentication, error)
+	GetRequestAuthentications(namespace, labelSelector string) ([]*security_v1beta1.RequestAuthentication, error)
+}
+
+// cacheLister combines a bunch of lister types into one.
+// This can probably be simplified or turned into an interface
+// with go generics.
+type cacheLister struct {
+	// Kube listers
+	configMapLister   core_v1_listers.ConfigMapLister
+	daemonSetLister   apps_v1_listers.DaemonSetLister
+	deploymentLister  apps_v1_listers.DeploymentLister
+	endpointLister    core_v1_listers.EndpointsLister
+	podLister         core_v1_listers.PodLister
+	replicaSetLister  apps_v1_listers.ReplicaSetLister
+	serviceLister     core_v1_listers.ServiceLister
+	statefulSetLister apps_v1_listers.StatefulSetLister
+
+	cachesSynced []cache.InformerSynced
+
+	// Istio listers
+	authzLister           istiosec_v1beta1_listers.AuthorizationPolicyLister
+	destinationRuleLister istionet_v1beta1_listers.DestinationRuleLister
+	envoyFilterLister     istionet_v1alpha3_listers.EnvoyFilterLister
+	gatewayLister         istionet_v1beta1_listers.GatewayLister
+	k8sgatewayLister      k8s_v1beta1_listers.GatewayLister
+	k8shttprouteLister    k8s_v1beta1_listers.HTTPRouteLister
+	peerAuthnLister       istiosec_v1beta1_listers.PeerAuthenticationLister
+	requestAuthnLister    istiosec_v1beta1_listers.RequestAuthenticationLister
+	serviceEntryLister    istionet_v1beta1_listers.ServiceEntryLister
+	sidecarLister         istionet_v1beta1_listers.SidecarLister
+	telemetryLister       istiotelem_v1alpha1_listers.TelemetryLister
+	virtualServiceLister  istionet_v1beta1_listers.VirtualServiceLister
+	wasmPluginLister      istioext_v1alpha1_listers.WasmPluginLister
+	workloadEntryLister   istionet_v1beta1_listers.WorkloadEntryLister
+	workloadGroupLister   istionet_v1beta1_listers.WorkloadGroupLister
+}
+
+// kubeCache is a local cache of kube objects. Manages informers and listers.
+type kubeCache struct {
+	cacheIstioTypes        map[string]bool
+	cacheLock              sync.RWMutex
+	cacheNamespacesRegexps []regexp.Regexp
+	cfg                    config.Config
+	client                 kubernetes.ClientInterface
+	clusterCacheLister     *cacheLister
+	clusterScoped          bool
+	nsCacheLister          map[string]*cacheLister
+	registryRefreshHandler RegistryRefreshHandler
+	refreshDuration        time.Duration
+	// Stops the cluster scoped informers when a refresh is necessary.
+	// Close this channel to stop the cluster-scoped informers.
+	stopClusterScopedChan chan struct{}
+	// Stops the namespace scoped informers when a refresh is necessary.
+	stopNSChans map[string]chan struct{}
+}
+
+// Starts all informers. These run until context is cancelled.
+func NewKubeCache(kialiClient kubernetes.ClientInterface, cfg config.Config, refreshHandler RegistryRefreshHandler, namespaceSeedList ...string) (*kubeCache, error) {
+	refreshDuration := time.Duration(cfg.KubernetesConfig.CacheDuration) * time.Second
+
+	cacheNamespaces := cfg.KubernetesConfig.CacheNamespaces
+	cacheIstioTypes := make(map[string]bool)
+	for _, iType := range cfg.KubernetesConfig.CacheIstioTypes {
+		cacheIstioTypes[iType] = true
+	}
+	log.Tracef("[Kiali Cache] cacheIstioTypes %v", cacheIstioTypes)
+
+	cacheNamespacesRegexps := make([]regexp.Regexp, len(cacheNamespaces))
+	for i, ns := range cacheNamespaces {
+		cacheNamespacesRegexps[i] = *regexp.MustCompile(strings.TrimSpace(ns))
+	}
+
+	c := &kubeCache{
+		cacheIstioTypes:        cacheIstioTypes,
+		cacheNamespacesRegexps: cacheNamespacesRegexps,
+		cfg:                    cfg,
+		client:                 kialiClient,
+		// Only when all namespaces are accessible should the cache be cluster scoped.
+		// Otherwise, kiali may not have access to all namespaces since
+		// the operator only grants clusterroles when all namespaces are accessible.
+		clusterScoped:          cfg.AllNamespacesAccessible(),
+		registryRefreshHandler: refreshHandler,
+		refreshDuration:        refreshDuration,
+	}
+
+	if c.clusterScoped {
+		log.Debug("[Kiali Cache] Using 'cluster' scoped Kiali Cache")
+		if err := c.startInformers(""); err != nil {
+			return nil, err
+		}
+	} else {
+		log.Debug("[Kiali Cache] Using 'namespace' scoped Kiali Cache")
+		c.nsCacheLister = make(map[string]*cacheLister)
+		c.stopNSChans = make(map[string]chan struct{})
+
+		for _, ns := range namespaceSeedList {
+			c.CheckNamespace(ns)
+		}
+	}
+
+	log.Infof("[Kiali Cache] Kube cache is active for namespaces %v", cacheNamespaces)
+
+	return c, nil
+}
+
+// It will indicate if a namespace should have a cache
+func (c *kubeCache) isCached(namespace string) bool {
+	for _, cacheNs := range c.cacheNamespacesRegexps {
+		if cacheNs.MatchString(namespace) {
+			return true
+		}
+	}
+	return false
+}
+
+// UpdateClient will update the client and refresh the cache.
+// This is used when the client is updated with a new token.
+func (c *kubeCache) UpdateClient(kialiClient kubernetes.ClientInterface) error {
+	log.Debug("[Kiali Cache] Updating Kiali client. Refreshing cache.")
+	c.cacheLock.Lock()
+	defer c.cacheLock.Unlock()
+
+	c.client = kialiClient
+	if c.clusterScoped {
+		if err := c.refresh(""); err != nil {
+			return err
+		}
+	} else {
+		for ns := range c.nsCacheLister {
+			if err := c.refresh(ns); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// CheckNamespace will
+// - Validate if a namespace is included in the cache
+// - Create and initialize a cache
+func (c *kubeCache) CheckNamespace(namespace string) bool {
+	if c.clusterScoped {
+		return true
+	} else if !c.isCached(namespace) {
+		return false
+	}
+
+	var isNSCached bool
+	// Separate func so we can defer
+	func() {
+		c.cacheLock.RLock()
+		defer c.cacheLock.RUnlock()
+		_, isNSCached = c.nsCacheLister[namespace]
+	}()
+
+	if !isNSCached {
+		c.cacheLock.Lock()
+		defer c.cacheLock.Unlock()
+		if err := c.startInformers(namespace); err != nil {
+			log.Errorf("[Kiali Cache] Error starting informers for namespace: %s. Err: %s", namespace, err)
+			return false
+		}
+	}
+
+	return true
+}
+
+// Stop will stop either the cluster wide cache or all of the namespace caches.
+func (c *kubeCache) Stop() {
+	log.Infof("Stopping Kiali Cache")
+	c.cacheLock.Lock()
+	defer c.cacheLock.Unlock()
+
+	if c.clusterScoped {
+		c.stop("")
+	} else {
+		for namespace := range c.stopNSChans {
+			c.stop(namespace)
+		}
+	}
+}
+
+func (c *kubeCache) stop(namespace string) {
+	if c.clusterScoped {
+		close(c.stopClusterScopedChan)
+	} else {
+		if nsChan, exist := c.stopNSChans[namespace]; exist {
+			close(nsChan)
+			delete(c.stopNSChans, namespace)
+			delete(c.nsCacheLister, namespace)
+		}
+	}
+}
+
+// Refresh will recreate the necessary cache. If the cache is cluster-scoped the "namespace" argument
+// is ignored and the whole cache is recreated, otherwise only the namespace-specific cache is updated.
+func (c *kubeCache) Refresh(namespace string) {
+	c.cacheLock.Lock()
+	defer c.cacheLock.Unlock()
+
+	if err := c.refresh(namespace); err != nil {
+		log.Errorf("[Kiali Cache] Error refreshing cache for namespace: %s. Err: %s", namespace, err)
+	}
+}
+
+func (c *kubeCache) refresh(namespace string) error {
+	if c.clusterScoped {
+		namespace = ""
+	}
+
+	c.stop(namespace)
+	return c.startInformers(namespace)
+}
+
+func (c *kubeCache) CheckIstioResource(resourceType string) bool {
+	_, exist := c.cacheIstioTypes[kubernetes.PluralType[resourceType]]
+	return exist
+}
+
+// starter is a small interface around the different informer factories that
+// allows us to start them all.
+type starter interface {
+	Start(stopCh <-chan struct{})
+}
+
+func (c *kubeCache) startInformers(namespace string) error {
+	informers := []starter{
+		c.createKubernetesInformers(namespace),
+		c.createIstioInformers(namespace),
+		c.createGatewayInformers(namespace),
+	}
+
+	var scope string
+	stop := make(chan struct{})
+	if c.clusterScoped {
+		scope = "cluster-scoped"
+		c.stopClusterScopedChan = stop
+	} else {
+		scope = fmt.Sprintf("namespace-scoped for namespace: %s", namespace)
+		c.stopNSChans[namespace] = stop
+	}
+
+	log.Debugf("[Kiali Cache] Starting %s informers", scope)
+	for _, informer := range informers {
+		go informer.Start(stop)
+	}
+
+	log.Infof("[Kiali Cache] Waiting for %s cache to sync", scope)
+	if !cache.WaitForCacheSync(stop, c.getCacheLister(namespace).cachesSynced...) {
+		log.Errorf("[Kiali Cache] Failed to sync %s cache", scope)
+		return errors.New("failed to sync cache")
+	}
+
+	log.Info("[Kiali Cache] Started")
+	return nil
+}
+
+func (c *kubeCache) createIstioInformers(namespace string) istio.SharedInformerFactory {
+	var opts []istio.SharedInformerOption
+	if namespace != "" {
+		opts = append(opts, istio.WithNamespace(namespace))
+	}
+
+	sharedInformers := istio.NewSharedInformerFactoryWithOptions(c.client.Istio(), c.refreshDuration, opts...)
+	lister := c.getCacheLister(namespace)
+
+	if c.CheckIstioResource(kubernetes.AuthorizationPolicies) {
+		lister.authzLister = sharedInformers.Security().V1beta1().AuthorizationPolicies().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Security().V1beta1().AuthorizationPolicies().Informer().HasSynced)
+		sharedInformers.Security().V1beta1().AuthorizationPolicies().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.DestinationRules) {
+		lister.destinationRuleLister = sharedInformers.Networking().V1beta1().DestinationRules().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1beta1().DestinationRules().Informer().HasSynced)
+		sharedInformers.Networking().V1beta1().DestinationRules().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.EnvoyFilters) {
+		lister.envoyFilterLister = sharedInformers.Networking().V1alpha3().EnvoyFilters().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1alpha3().EnvoyFilters().Informer().HasSynced)
+		sharedInformers.Networking().V1alpha3().EnvoyFilters().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.Gateways) {
+		lister.gatewayLister = sharedInformers.Networking().V1beta1().Gateways().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1beta1().Gateways().Informer().HasSynced)
+		sharedInformers.Networking().V1beta1().Gateways().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.PeerAuthentications) {
+		lister.peerAuthnLister = sharedInformers.Security().V1beta1().PeerAuthentications().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Security().V1beta1().PeerAuthentications().Informer().HasSynced)
+		sharedInformers.Security().V1beta1().PeerAuthentications().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.RequestAuthentications) {
+		lister.requestAuthnLister = sharedInformers.Security().V1beta1().RequestAuthentications().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Security().V1beta1().RequestAuthentications().Informer().HasSynced)
+		sharedInformers.Security().V1beta1().RequestAuthentications().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.ServiceEntries) {
+		lister.serviceEntryLister = sharedInformers.Networking().V1beta1().ServiceEntries().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1beta1().ServiceEntries().Informer().HasSynced)
+		sharedInformers.Networking().V1beta1().ServiceEntries().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.Sidecars) {
+		lister.sidecarLister = sharedInformers.Networking().V1beta1().Sidecars().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1beta1().Sidecars().Informer().HasSynced)
+		sharedInformers.Networking().V1beta1().Sidecars().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.Telemetries) {
+		lister.telemetryLister = sharedInformers.Telemetry().V1alpha1().Telemetries().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Telemetry().V1alpha1().Telemetries().Informer().HasSynced)
+		sharedInformers.Telemetry().V1alpha1().Telemetries().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.VirtualServices) {
+		lister.virtualServiceLister = sharedInformers.Networking().V1beta1().VirtualServices().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1beta1().VirtualServices().Informer().HasSynced)
+		sharedInformers.Networking().V1beta1().VirtualServices().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.WasmPlugins) {
+		lister.wasmPluginLister = sharedInformers.Extensions().V1alpha1().WasmPlugins().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Extensions().V1alpha1().WasmPlugins().Informer().HasSynced)
+		sharedInformers.Extensions().V1alpha1().WasmPlugins().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.WorkloadEntries) {
+		lister.workloadEntryLister = sharedInformers.Networking().V1beta1().WorkloadEntries().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1beta1().WorkloadEntries().Informer().HasSynced)
+		sharedInformers.Networking().V1beta1().WorkloadEntries().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+	if c.CheckIstioResource(kubernetes.WorkloadGroups) {
+		lister.workloadGroupLister = sharedInformers.Networking().V1beta1().WorkloadGroups().Lister()
+		lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Networking().V1beta1().WorkloadGroups().Informer().HasSynced)
+		sharedInformers.Networking().V1beta1().WorkloadGroups().Informer().AddEventHandler(c.registryRefreshHandler)
+	}
+
+	return sharedInformers
+}
+
+func (c *kubeCache) createGatewayInformers(namespace string) gateway.SharedInformerFactory {
+	sharedInformers := gateway.NewSharedInformerFactory(c.client.GatewayAPI(), c.refreshDuration)
+	lister := c.getCacheLister(namespace)
+
+	if c.client.IsGatewayAPI() {
+		if c.CheckIstioResource(kubernetes.K8sGateways) {
+			lister.k8sgatewayLister = sharedInformers.Gateway().V1beta1().Gateways().Lister()
+			lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Gateway().V1beta1().Gateways().Informer().HasSynced)
+			sharedInformers.Gateway().V1beta1().Gateways().Informer().AddEventHandler(c.registryRefreshHandler)
+		}
+		if c.CheckIstioResource(kubernetes.K8sHTTPRoutes) {
+			lister.k8shttprouteLister = sharedInformers.Gateway().V1beta1().HTTPRoutes().Lister()
+			lister.cachesSynced = append(lister.cachesSynced, sharedInformers.Gateway().V1beta1().HTTPRoutes().Informer().HasSynced)
+			sharedInformers.Gateway().V1beta1().Gateways().Informer().AddEventHandler(c.registryRefreshHandler)
+		}
+	}
+	return sharedInformers
+}
+
+// createKubernetesInformers creates kube informers for all objects kiali watches and
+// saves them to the typeCache. If namespace is not empty, the informers are scoped
+// to the namespace. Otherwise, the informers are cluster-wide.
+func (c *kubeCache) createKubernetesInformers(namespace string) informers.SharedInformerFactory {
+	var opts []informers.SharedInformerOption
+	if namespace != "" {
+		opts = append(opts, informers.WithNamespace(namespace))
+	}
+
+	sharedInformers := informers.NewSharedInformerFactoryWithOptions(c.client.Kube(), c.refreshDuration, opts...)
+
+	lister := &cacheLister{
+		deploymentLister:  sharedInformers.Apps().V1().Deployments().Lister(),
+		statefulSetLister: sharedInformers.Apps().V1().StatefulSets().Lister(),
+		daemonSetLister:   sharedInformers.Apps().V1().DaemonSets().Lister(),
+		serviceLister:     sharedInformers.Core().V1().Services().Lister(),
+		endpointLister:    sharedInformers.Core().V1().Endpoints().Lister(),
+		podLister:         sharedInformers.Core().V1().Pods().Lister(),
+		replicaSetLister:  sharedInformers.Apps().V1().ReplicaSets().Lister(),
+		configMapLister:   sharedInformers.Core().V1().ConfigMaps().Lister(),
+	}
+	lister.cachesSynced = append(lister.cachesSynced,
+		sharedInformers.Apps().V1().Deployments().Informer().HasSynced,
+		sharedInformers.Apps().V1().StatefulSets().Informer().HasSynced,
+		sharedInformers.Apps().V1().DaemonSets().Informer().HasSynced,
+		sharedInformers.Core().V1().Services().Informer().HasSynced,
+		sharedInformers.Core().V1().Endpoints().Informer().HasSynced,
+		sharedInformers.Core().V1().Pods().Informer().HasSynced,
+		sharedInformers.Apps().V1().ReplicaSets().Informer().HasSynced,
+		sharedInformers.Core().V1().ConfigMaps().Informer().HasSynced,
+	)
+	sharedInformers.Core().V1().Services().Informer().AddEventHandler(c.registryRefreshHandler)
+	sharedInformers.Core().V1().Endpoints().Informer().AddEventHandler(c.registryRefreshHandler)
+
+	if c.clusterScoped {
+		c.clusterCacheLister = lister
+	} else {
+		c.nsCacheLister[namespace] = lister
+	}
+
+	return sharedInformers
+}
+
+func (c *kubeCache) getCacheLister(namespace string) *cacheLister {
+	if c.clusterScoped {
+		return c.clusterCacheLister
+	}
+	return c.nsCacheLister[namespace]
+}
+
+func (c *kubeCache) GetConfigMap(namespace, name string) (*core_v1.ConfigMap, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	log.Tracef("[Kiali Cache] Get [resource: ConfigMap] for [namespace: %s] [name: %s]", namespace, name)
+	cfg, err := c.getCacheLister(namespace).configMapLister.ConfigMaps(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retCM := cfg.DeepCopy()
+	retCM.Kind = kubernetes.ConfigMapType
+	return retCM, nil
+}
+
+func (c *kubeCache) GetDaemonSets(namespace string) ([]apps_v1.DaemonSet, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	daemonSets, err := c.getCacheLister(namespace).daemonSetLister.DaemonSets(namespace).List(labels.Everything())
+	if err != nil {
+		return nil, err
+	}
+	log.Tracef("[Kiali Cache] Get [resource: DaemonSet] for [namespace: %s] = %d", namespace, len(daemonSets))
+
+	retSets := []apps_v1.DaemonSet{}
+	for _, ds := range daemonSets {
+		// Do not modify what is returned by the lister since that is shared and will cause data races.
+		d := ds.DeepCopy()
+		d.Kind = kubernetes.DaemonSetType
+		retSets = append(retSets, *d)
+	}
+	return retSets, nil
+}
+
+func (c *kubeCache) GetDaemonSet(namespace, name string) (*apps_v1.DaemonSet, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	log.Tracef("[Kiali Cache] Get [resource: DaemonSet] for [namespace: %s] [name: %s]", namespace, name)
+	ds, err := c.getCacheLister(namespace).daemonSetLister.DaemonSets(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retDS := ds.DeepCopy()
+	retDS.Kind = kubernetes.DaemonSetType
+	return retDS, nil
+}
+
+func (c *kubeCache) GetDeployments(namespace string) ([]apps_v1.Deployment, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	deployments, err := c.getCacheLister(namespace).deploymentLister.Deployments(namespace).List(labels.Everything())
+	if err != nil {
+		return nil, err
+	}
+	log.Tracef("[Kiali Cache] Get [resource: Deployment] for [namespace: %s] = %d", namespace, len(deployments))
+
+	retDeployments := []apps_v1.Deployment{}
+	for _, deployment := range deployments {
+		// Do not modify what is returned by the lister since that is shared and will cause data races.
+		d := deployment.DeepCopy()
+		d.Kind = kubernetes.DeploymentType
+		retDeployments = append(retDeployments, *d)
+	}
+	return retDeployments, nil
+}
+
+func (c *kubeCache) GetDeployment(namespace, name string) (*apps_v1.Deployment, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	log.Tracef("[Kiali Cache] Get [resource: Deployment] for [namespace: %s] [name: %s]", namespace, name)
+	deployment, err := c.getCacheLister(namespace).deploymentLister.Deployments(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retDep := deployment.DeepCopy()
+	retDep.Kind = kubernetes.DeploymentType
+	return retDep, nil
+}
+
+func (c *kubeCache) GetEndpoints(namespace, name string) (*core_v1.Endpoints, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	log.Tracef("[Kiali Cache] Get [resource: Endpoints] for [namespace: %s] [name: %s]", namespace, name)
+	endpoints, err := c.getCacheLister(namespace).endpointLister.Endpoints(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retEnd := endpoints.DeepCopy()
+	retEnd.Kind = kubernetes.EndpointsType
+	return retEnd, nil
+}
+
+func (c *kubeCache) GetStatefulSets(namespace string) ([]apps_v1.StatefulSet, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	statefulSets, err := c.getCacheLister(namespace).statefulSetLister.StatefulSets(namespace).List(labels.Everything())
+	if err != nil {
+		return nil, err
+	}
+	log.Tracef("[Kiali Cache] Get [resource: StatefulSet] for [namespace: %s] = %d", namespace, len(statefulSets))
+
+	retSets := []apps_v1.StatefulSet{}
+	for _, ss := range statefulSets {
+		// Do not modify what is returned by the lister since that is shared and will cause data races.
+		s := ss.DeepCopy()
+		s.Kind = kubernetes.StatefulSetType
+		retSets = append(retSets, *s)
+	}
+	return retSets, nil
+}
+
+func (c *kubeCache) GetStatefulSet(namespace, name string) (*apps_v1.StatefulSet, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	log.Tracef("[Kiali Cache] Get [resource: StatefulSet] for [namespace: %s] [name: %s]", namespace, name)
+	statefulSet, err := c.getCacheLister(namespace).statefulSetLister.StatefulSets(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retSet := statefulSet.DeepCopy()
+	retSet.Kind = kubernetes.StatefulSetType
+	return retSet, nil
+}
+
+func (c *kubeCache) GetServices(namespace string, selectorLabels map[string]string) ([]core_v1.Service, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	services, err := c.getCacheLister(namespace).serviceLister.Services(namespace).List(labels.Set(selectorLabels).AsSelector())
+	if err != nil {
+		return nil, err
+	}
+	log.Tracef("[Kiali Cache] Get [resource: Service] for [namespace: %s] = %d", namespace, len(services))
+
+	retServices := []core_v1.Service{}
+	for _, service := range services {
+		// Do not modify what is returned by the lister since that is shared and will cause data races.
+		svc := service.DeepCopy()
+		svc.Kind = kubernetes.ServiceType
+		retServices = append(retServices, *svc)
+	}
+	return retServices, nil
+}
+
+func (c *kubeCache) GetService(namespace, name string) (*core_v1.Service, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	log.Tracef("[Kiali Cache] Get [resource: Service] for [namespace: %s] [name: %s]", namespace, name)
+	service, err := c.getCacheLister(namespace).serviceLister.Services(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retSvc := service.DeepCopy()
+	retSvc.Kind = kubernetes.ServiceType
+	return retSvc, nil
+}
+
+func (c *kubeCache) GetPods(namespace, labelSelector string) ([]core_v1.Pod, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	pods, err := c.getCacheLister(namespace).podLister.Pods(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+	log.Tracef("[Kiali Cache] Get [resource: Pod] for [namespace: %s] = %d", namespace, len(pods))
+
+	retPods := []core_v1.Pod{}
+	for _, pod := range pods {
+		// Do not modify what is returned by the lister since that is shared and will cause data races.
+		p := pod.DeepCopy()
+		p.Kind = kubernetes.PodType
+		retPods = append(retPods, *p)
+	}
+	return retPods, nil
+}
+
+// GetReplicaSets returns the cached ReplicaSets for the namespace.  For any given RS for a given
+// Owner (i.e. Deployment), only the most recent version of the RS will be included in the returned list.
+// When an owning Deployment is configured with revisionHistoryLimit > 0, then k8s may return multiple
+// versions of the RS for the same Deployment (current and older revisions). Note that it is still possible
+// to have multiple RS for the same owner. In which case the most recent version of each is returned.
+// see also: ../kubernetes.go
+func (c *kubeCache) GetReplicaSets(namespace string) ([]apps_v1.ReplicaSet, error) {
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	reps, err := c.getCacheLister(namespace).replicaSetLister.ReplicaSets(namespace).List(labels.Everything())
+	if err != nil {
+		return nil, err
+	}
+
+	result := []apps_v1.ReplicaSet{}
+	if len(reps) > 0 {
+		activeRSMap := map[string]*apps_v1.ReplicaSet{}
+		for _, rs := range reps {
+			if len(rs.OwnerReferences) > 0 {
+				for _, ownerRef := range rs.OwnerReferences {
+					if ownerRef.Controller != nil && *ownerRef.Controller {
+						key := fmt.Sprintf("%s_%s_%s", ownerRef.Name, rs.Name, rs.ResourceVersion)
+						if currRS, ok := activeRSMap[key]; ok {
+							if currRS.CreationTimestamp.Time.Before(rs.CreationTimestamp.Time) {
+								activeRSMap[key] = rs
+							}
+						} else {
+							activeRSMap[key] = rs
+						}
+					}
+				}
+			} else {
+				// it is it's own controller
+				activeRSMap[rs.Name] = rs
+			}
+		}
+
+		lenRS := len(activeRSMap)
+		result = make([]apps_v1.ReplicaSet, lenRS)
+		i := 0
+		for _, activeRS := range activeRSMap {
+			// Do not modify what is returned by the lister since that is shared and will cause data races.
+			rs := activeRS.DeepCopy()
+			rs.Kind = kubernetes.ReplicaSetType
+			result[i] = *rs
+			i = i + 1
+		}
+		log.Tracef("[Kiali Cache] Get [resource: ReplicaSet] for [namespace: %s] = %d", namespace, lenRS)
+	}
+	return result, nil
+}
+
+func (c *kubeCache) GetDestinationRule(namespace, name string) (*networking_v1beta1.DestinationRule, error) {
+	if !c.CheckIstioResource(kubernetes.DestinationRules) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.DestinationRuleType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	dr, err := c.getCacheLister(namespace).destinationRuleLister.DestinationRules(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retDR := dr.DeepCopy()
+	retDR.Kind = kubernetes.DestinationRuleType
+	return retDR, nil
+}
+
+func (c *kubeCache) GetDestinationRules(namespace, labelSelector string) ([]*networking_v1beta1.DestinationRule, error) {
+	if !c.CheckIstioResource(kubernetes.DestinationRules) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.DestinationRuleType)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	drs, err := c.getCacheLister(namespace).destinationRuleLister.DestinationRules(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if drs == nil {
+		return []*networking_v1beta1.DestinationRule{}, nil
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	var retDRs []*networking_v1beta1.DestinationRule
+	for _, dr := range drs {
+		d := dr.DeepCopy()
+		d.Kind = kubernetes.DestinationRuleType
+		retDRs = append(retDRs, d)
+	}
+	return retDRs, nil
+}
+
+func (c *kubeCache) GetEnvoyFilter(namespace, name string) (*networking_v1alpha3.EnvoyFilter, error) {
+	if !c.CheckIstioResource(kubernetes.EnvoyFilters) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.EnvoyFilterType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	ef, err := c.getCacheLister(namespace).envoyFilterLister.EnvoyFilters(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	// Do not modify what is returned by the lister since that is shared and will cause data races.
+	retEF := ef.DeepCopy()
+	retEF.Kind = kubernetes.EnvoyFilterType
+	return retEF, nil
+}
+
+func (c *kubeCache) GetEnvoyFilters(namespace, labelSelector string) ([]*networking_v1alpha3.EnvoyFilter, error) {
+	if !c.CheckIstioResource(kubernetes.EnvoyFilters) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.EnvoyFilterType)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	efs, err := c.getCacheLister(namespace).envoyFilterLister.EnvoyFilters(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if efs == nil {
+		return []*networking_v1alpha3.EnvoyFilter{}, nil
+	}
+
+	var retEFs []*networking_v1alpha3.EnvoyFilter
+	for _, ef := range efs {
+		e := ef.DeepCopy()
+		e.Kind = kubernetes.EnvoyFilterType
+		retEFs = append(retEFs, e)
+	}
+	return retEFs, nil
+}
+
+func (c *kubeCache) GetGateway(namespace, name string) (*networking_v1beta1.Gateway, error) {
+	if !c.CheckIstioResource(kubernetes.Gateways) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.GatewayType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	gw, err := c.getCacheLister(namespace).gatewayLister.Gateways(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retGW := gw.DeepCopy()
+	retGW.Kind = kubernetes.GatewayType
+	return retGW, nil
+}
+
+func (c *kubeCache) GetGateways(namespace, labelSelector string) ([]*networking_v1beta1.Gateway, error) {
+	if !c.CheckIstioResource(kubernetes.Gateways) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.Gateways)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	gateways, err := c.getCacheLister(namespace).gatewayLister.Gateways(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if gateways == nil {
+		return []*networking_v1beta1.Gateway{}, nil
+	}
+
+	var retGateways []*networking_v1beta1.Gateway
+	for _, gw := range gateways {
+		g := gw.DeepCopy()
+		g.Kind = kubernetes.GatewayType
+		retGateways = append(retGateways, g)
+	}
+	return retGateways, nil
+}
+
+func (c *kubeCache) GetServiceEntry(namespace, name string) (*networking_v1beta1.ServiceEntry, error) {
+	if !c.CheckIstioResource(kubernetes.ServiceEntries) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.ServiceEntryType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	se, err := c.getCacheLister(namespace).serviceEntryLister.ServiceEntries(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retSE := se.DeepCopy()
+	retSE.Kind = kubernetes.ServiceEntryType
+	return retSE, nil
+}
+
+func (c *kubeCache) GetServiceEntries(namespace, labelSelector string) ([]*networking_v1beta1.ServiceEntry, error) {
+	if !c.CheckIstioResource(kubernetes.ServiceEntries) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.ServiceEntryType)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	ses, err := c.getCacheLister(namespace).serviceEntryLister.ServiceEntries(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if ses == nil {
+		return []*networking_v1beta1.ServiceEntry{}, nil
+	}
+
+	var retSEs []*networking_v1beta1.ServiceEntry
+	for _, se := range ses {
+		s := se.DeepCopy()
+		s.Kind = kubernetes.ServiceEntryType
+		retSEs = append(retSEs, s)
+	}
+	return retSEs, nil
+}
+
+func (c *kubeCache) GetSidecar(namespace, name string) (*networking_v1beta1.Sidecar, error) {
+	if !c.CheckIstioResource(kubernetes.Sidecars) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.SidecarType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	sc, err := c.getCacheLister(namespace).sidecarLister.Sidecars(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retSC := sc.DeepCopy()
+	retSC.Kind = kubernetes.SidecarType
+	return retSC, nil
+}
+
+func (c *kubeCache) GetSidecars(namespace, labelSelector string) ([]*networking_v1beta1.Sidecar, error) {
+	if !c.CheckIstioResource(kubernetes.Sidecars) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.SidecarType)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	sidecars, err := c.getCacheLister(namespace).sidecarLister.Sidecars(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if sidecars == nil {
+		return []*networking_v1beta1.Sidecar{}, nil
+	}
+
+	var retSC []*networking_v1beta1.Sidecar
+	for _, sc := range sidecars {
+		s := sc.DeepCopy()
+		s.Kind = kubernetes.SidecarType
+		retSC = append(retSC, s)
+	}
+	return retSC, nil
+}
+
+func (c *kubeCache) GetVirtualService(namespace, name string) (*networking_v1beta1.VirtualService, error) {
+	if !c.CheckIstioResource(kubernetes.VirtualServices) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.VirtualServiceType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	vs, err := c.getCacheLister(namespace).virtualServiceLister.VirtualServices(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retVS := vs.DeepCopy()
+	retVS.Kind = kubernetes.VirtualServiceType
+	return retVS, nil
+}
+
+func (c *kubeCache) GetVirtualServices(namespace, labelSelector string) ([]*networking_v1beta1.VirtualService, error) {
+	if !c.CheckIstioResource(kubernetes.VirtualServices) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.VirtualServiceType)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	vs, err := c.getCacheLister(namespace).virtualServiceLister.VirtualServices(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if vs == nil {
+		return []*networking_v1beta1.VirtualService{}, nil
+	}
+
+	var retVS []*networking_v1beta1.VirtualService
+	for _, v := range vs {
+		vv := v.DeepCopy()
+		vv.Kind = kubernetes.VirtualServiceType
+		retVS = append(retVS, vv)
+	}
+	return retVS, nil
+}
+
+func (c *kubeCache) GetWorkloadEntry(namespace, name string) (*networking_v1beta1.WorkloadEntry, error) {
+	if !c.CheckIstioResource(kubernetes.WorkloadEntries) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadEntryType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	we, err := c.getCacheLister(namespace).workloadEntryLister.WorkloadEntries(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retWE := we.DeepCopy()
+	retWE.Kind = kubernetes.WorkloadEntryType
+	return retWE, nil
+}
+
+func (c *kubeCache) GetWorkloadEntries(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadEntry, error) {
+	if !c.CheckIstioResource(kubernetes.WorkloadEntries) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadEntryType)
+	}
+
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	we, err := c.getCacheLister(namespace).workloadEntryLister.WorkloadEntries(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if we == nil {
+		return []*networking_v1beta1.WorkloadEntry{}, nil
+	}
+
+	var retWE []*networking_v1beta1.WorkloadEntry
+	for _, w := range we {
+		ww := w.DeepCopy()
+		ww.Kind = kubernetes.WorkloadEntryType
+		retWE = append(retWE, ww)
+	}
+	return retWE, nil
+}
+
+func (c *kubeCache) GetWorkloadGroup(namespace, name string) (*networking_v1beta1.WorkloadGroup, error) {
+	if !c.CheckIstioResource(kubernetes.WorkloadGroups) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadGroupType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	wg, err := c.getCacheLister(namespace).workloadGroupLister.WorkloadGroups(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retWG := wg.DeepCopy()
+	retWG.Kind = kubernetes.WorkloadGroupType
+	return retWG, nil
+}
+
+func (c *kubeCache) GetWorkloadGroups(namespace, labelSelector string) ([]*networking_v1beta1.WorkloadGroup, error) {
+	if !c.CheckIstioResource(kubernetes.WorkloadGroups) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WorkloadGroups)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	wg, err := c.getCacheLister(namespace).workloadGroupLister.WorkloadGroups(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if wg == nil {
+		return []*networking_v1beta1.WorkloadGroup{}, nil
+	}
+
+	var retWG []*networking_v1beta1.WorkloadGroup
+	for _, w := range wg {
+		ww := w.DeepCopy()
+		ww.Kind = kubernetes.WorkloadGroupType
+		retWG = append(retWG, ww)
+	}
+	return retWG, nil
+}
+
+func (c *kubeCache) GetWasmPlugin(namespace, name string) (*extentions_v1alpha1.WasmPlugin, error) {
+	if !c.CheckIstioResource(kubernetes.WasmPlugins) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WasmPluginType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	wp, err := c.getCacheLister(namespace).wasmPluginLister.WasmPlugins(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retWP := wp.DeepCopy()
+	retWP.Kind = kubernetes.WasmPluginType
+	return retWP, nil
+}
+
+func (c *kubeCache) GetWasmPlugins(namespace, labelSelector string) ([]*extentions_v1alpha1.WasmPlugin, error) {
+	if !c.CheckIstioResource(kubernetes.WasmPlugins) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.WasmPlugins)
+	}
+
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	wp, err := c.getCacheLister(namespace).wasmPluginLister.WasmPlugins(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if wp == nil {
+		return []*extentions_v1alpha1.WasmPlugin{}, nil
+	}
+
+	var retWP []*extentions_v1alpha1.WasmPlugin
+	for _, w := range wp {
+		ww := w.DeepCopy()
+		ww.Kind = kubernetes.WasmPluginType
+		retWP = append(retWP, ww)
+	}
+	return retWP, nil
+}
+
+func (c *kubeCache) GetTelemetry(namespace, name string) (*v1alpha1.Telemetry, error) {
+	if !c.CheckIstioResource(kubernetes.Telemetries) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.TelemetryType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	t, err := c.getCacheLister(namespace).telemetryLister.Telemetries(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retT := t.DeepCopy()
+	retT.Kind = kubernetes.TelemetryType
+	return retT, nil
+}
+
+func (c *kubeCache) GetTelemetries(namespace, labelSelector string) ([]*v1alpha1.Telemetry, error) {
+	if !c.CheckIstioResource(kubernetes.Telemetries) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.Telemetries)
+	}
+
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	t, err := c.getCacheLister(namespace).telemetryLister.Telemetries(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if t == nil {
+		return []*v1alpha1.Telemetry{}, nil
+	}
+
+	var retT []*v1alpha1.Telemetry
+	for _, w := range t {
+		tt := w.DeepCopy()
+		tt.Kind = kubernetes.TelemetryType
+		retT = append(retT, tt)
+	}
+
+	return retT, nil
+}
+
+func (c *kubeCache) GetK8sGateway(namespace, name string) (*gatewayapi_v1beta1.Gateway, error) {
+	if !c.CheckIstioResource(kubernetes.K8sGateways) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sGatewayType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	g, err := c.getCacheLister(namespace).k8sgatewayLister.Gateways(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retG := g.DeepCopy()
+	retG.Kind = kubernetes.K8sGatewayType
+	return retG, nil
+}
+
+func (c *kubeCache) GetK8sGateways(namespace, labelSelector string) ([]*gatewayapi_v1beta1.Gateway, error) {
+	if !c.CheckIstioResource(kubernetes.K8sGateways) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sGateways)
+	}
+
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	g, err := c.getCacheLister(namespace).k8sgatewayLister.Gateways(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if g == nil {
+		return []*gatewayapi_v1beta1.Gateway{}, nil
+	}
+
+	var retG []*gatewayapi_v1beta1.Gateway
+	for _, w := range g {
+		gg := w.DeepCopy()
+		gg.Kind = kubernetes.K8sGatewayType
+		retG = append(retG, gg)
+	}
+
+	return retG, nil
+}
+
+func (c *kubeCache) GetK8sHTTPRoute(namespace, name string) (*gatewayapi_v1beta1.HTTPRoute, error) {
+	if !c.CheckIstioResource(kubernetes.K8sHTTPRoutes) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sHTTPRouteType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	g, err := c.getCacheLister(namespace).k8shttprouteLister.HTTPRoutes(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retG := g.DeepCopy()
+	retG.Kind = kubernetes.K8sHTTPRouteType
+	return retG, nil
+}
+
+func (c *kubeCache) GetK8sHTTPRoutes(namespace, labelSelector string) ([]*gatewayapi_v1beta1.HTTPRoute, error) {
+	if !c.CheckIstioResource(kubernetes.K8sHTTPRoutes) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.K8sHTTPRoutes)
+	}
+
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	r, err := c.getCacheLister(namespace).k8shttprouteLister.HTTPRoutes(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if r == nil {
+		return []*gatewayapi_v1beta1.HTTPRoute{}, nil
+	}
+
+	var retRoutes []*gatewayapi_v1beta1.HTTPRoute
+	for _, w := range r {
+		ww := w.DeepCopy()
+		ww.Kind = kubernetes.K8sHTTPRouteType
+		retRoutes = append(retRoutes, ww)
+	}
+
+	return retRoutes, nil
+}
+
+func (c *kubeCache) GetAuthorizationPolicy(namespace, name string) (*security_v1beta1.AuthorizationPolicy, error) {
+	if !c.CheckIstioResource(kubernetes.AuthorizationPolicies) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.AuthorizationPoliciesType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	ap, err := c.getCacheLister(namespace).authzLister.AuthorizationPolicies(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retAP := ap.DeepCopy()
+	retAP.Kind = kubernetes.AuthorizationPoliciesType
+	return retAP, nil
+}
+
+func (c *kubeCache) GetAuthorizationPolicies(namespace, labelSelector string) ([]*security_v1beta1.AuthorizationPolicy, error) {
+	if !c.CheckIstioResource(kubernetes.AuthorizationPolicies) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.AuthorizationPolicies)
+	}
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	authPolicies, err := c.getCacheLister(namespace).authzLister.AuthorizationPolicies(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if authPolicies == nil {
+		return []*security_v1beta1.AuthorizationPolicy{}, nil
+	}
+
+	var retAPs []*security_v1beta1.AuthorizationPolicy
+	for _, ap := range authPolicies {
+		a := ap.DeepCopy()
+		a.Kind = kubernetes.AuthorizationPoliciesType
+		retAPs = append(retAPs, a)
+	}
+	return retAPs, nil
+}
+
+func (c *kubeCache) GetPeerAuthentication(namespace, name string) (*security_v1beta1.PeerAuthentication, error) {
+	if !c.CheckIstioResource(kubernetes.PeerAuthentications) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.PeerAuthenticationsType)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	pa, err := c.getCacheLister(namespace).peerAuthnLister.PeerAuthentications(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retPA := pa.DeepCopy()
+	retPA.Kind = kubernetes.PeerAuthenticationsType
+	return retPA, nil
+}
+
+func (c *kubeCache) GetPeerAuthentications(namespace, labelSelector string) ([]*security_v1beta1.PeerAuthentication, error) {
+	if !c.CheckIstioResource(kubernetes.PeerAuthentications) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.PeerAuthenticationsType)
+	}
+
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	peerAuths, err := c.getCacheLister(namespace).peerAuthnLister.PeerAuthentications(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if peerAuths == nil {
+		return []*security_v1beta1.PeerAuthentication{}, nil
+	}
+
+	var retPAs []*security_v1beta1.PeerAuthentication
+	for _, pa := range peerAuths {
+		p := pa.DeepCopy()
+		p.Kind = kubernetes.PeerAuthenticationsType
+		retPAs = append(retPAs, p)
+	}
+	return retPAs, nil
+}
+
+func (c *kubeCache) GetRequestAuthentication(namespace, name string) (*security_v1beta1.RequestAuthentication, error) {
+	if !c.CheckIstioResource(kubernetes.RequestAuthentications) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.RequestAuthentications)
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	ra, err := c.getCacheLister(namespace).requestAuthnLister.RequestAuthentications(namespace).Get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	retRA := ra.DeepCopy()
+	retRA.Kind = kubernetes.RequestAuthenticationsType
+	return retRA, nil
+}
+
+func (c *kubeCache) GetRequestAuthentications(namespace, labelSelector string) ([]*security_v1beta1.RequestAuthentication, error) {
+	if !c.CheckIstioResource(kubernetes.RequestAuthentications) {
+		return nil, fmt.Errorf("Kiali cache doesn't support [resourceType: %s]", kubernetes.RequestAuthenticationsType)
+	}
+
+	selector, err := labels.Parse(labelSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read lock will prevent the cache from being refreshed while we are reading from the lister
+	// but it won't prevent other routines from reading from the lister.
+	defer c.cacheLock.RUnlock()
+	c.cacheLock.RLock()
+	reqAuths, err := c.getCacheLister(namespace).requestAuthnLister.RequestAuthentications(namespace).List(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lister returns nil when there are no results but callers of the cache expect an empty array
+	// so keeping the behavior the same since it matters for json marshalling.
+	if reqAuths == nil {
+		return []*security_v1beta1.RequestAuthentication{}, nil
+	}
+
+	var retRAs []*security_v1beta1.RequestAuthentication
+	for _, ra := range reqAuths {
+		r := ra.DeepCopy()
+		r.Kind = kubernetes.RequestAuthenticationsType
+		retRAs = append(retRAs, r)
+	}
+	return retRAs, nil
+}
diff --git a/kubernetes/cache/kubernetes.go b/kubernetes/cache/kubernetes.go
deleted file mode 100644
index 480ce8d6b..000000000
--- a/kubernetes/cache/kubernetes.go
+++ /dev/null
@@ -1,263 +0,0 @@
-package cache
-
-import (
-	"fmt"
-
-	apps_v1 "k8s.io/api/apps/v1"
-	core_v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/client-go/informers"
-
-	"github.com/kiali/kiali/kubernetes"
-	"github.com/kiali/kiali/log"
-)
-
-type (
-	KubernetesCache interface {
-		GetConfigMap(namespace, name string) (*core_v1.ConfigMap, error)
-		GetDaemonSets(namespace string) ([]apps_v1.DaemonSet, error)
-		GetDaemonSet(namespace, name string) (*apps_v1.DaemonSet, error)
-		GetDeployments(namespace string) ([]apps_v1.Deployment, error)
-		GetDeployment(namespace, name string) (*apps_v1.Deployment, error)
-		GetEndpoints(namespace, name string) (*core_v1.Endpoints, error)
-		GetStatefulSets(namespace string) ([]apps_v1.StatefulSet, error)
-		GetStatefulSet(namespace, name string) (*apps_v1.StatefulSet, error)
-		GetServices(namespace string, selectorLabels map[string]string) ([]core_v1.Service, error)
-		GetService(namespace string, name string) (*core_v1.Service, error)
-		GetPods(namespace, labelSelector string) ([]core_v1.Pod, error)
-		GetReplicaSets(namespace string) ([]apps_v1.ReplicaSet, error)
-	}
-)
-
-// createKubernetesInformers creates kube informers for all objects kiali watches and
-// saves them to the typeCache. If namespace is not empty, the informers are scoped
-// to the namespace. Otherwise, the informers are cluster-wide.
-func (c *kialiCacheImpl) createKubernetesInformers(namespace string) informers.SharedInformerFactory {
-	var opts []informers.SharedInformerOption
-	if namespace != "" {
-		opts = append(opts, informers.WithNamespace(namespace))
-	}
-	sharedInformers := informers.NewSharedInformerFactoryWithOptions(c.k8sApi, c.refreshDuration, opts...)
-
-	lister := &cacheLister{
-		deploymentLister:  sharedInformers.Apps().V1().Deployments().Lister(),
-		statefulSetLister: sharedInformers.Apps().V1().StatefulSets().Lister(),
-		daemonSetLister:   sharedInformers.Apps().V1().DaemonSets().Lister(),
-		serviceLister:     sharedInformers.Core().V1().Services().Lister(),
-		endpointLister:    sharedInformers.Core().V1().Endpoints().Lister(),
-		podLister:         sharedInformers.Core().V1().Pods().Lister(),
-		replicaSetLister:  sharedInformers.Apps().V1().ReplicaSets().Lister(),
-		configMapLister:   sharedInformers.Core().V1().ConfigMaps().Lister(),
-	}
-	sharedInformers.Core().V1().Services().Informer().AddEventHandler(c.registryRefreshHandler)
-	sharedInformers.Core().V1().Endpoints().Informer().AddEventHandler(c.registryRefreshHandler)
-
-	if c.clusterScoped {
-		c.clusterCacheLister = lister
-	} else {
-		c.nsCacheLister[namespace] = lister
-	}
-
-	return sharedInformers
-}
-
-func (c *kialiCacheImpl) getCacheLister(namespace string) *cacheLister {
-	if c.clusterScoped {
-		return c.clusterCacheLister
-	}
-	return c.nsCacheLister[namespace]
-}
-
-func (c *kialiCacheImpl) GetConfigMap(namespace, name string) (*core_v1.ConfigMap, error) {
-	log.Tracef("[Kiali Cache] Get [resource: ConfigMap] for [namespace: %s] [name: %s]", namespace, name)
-	cfg, err := c.getCacheLister(namespace).configMapLister.ConfigMaps(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	cfg.Kind = kubernetes.ConfigMapType
-	return cfg, nil
-}
-
-func (c *kialiCacheImpl) GetDaemonSets(namespace string) ([]apps_v1.DaemonSet, error) {
-	daemonSets, err := c.getCacheLister(namespace).daemonSetLister.DaemonSets(namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-	log.Tracef("[Kiali Cache] Get [resource: DaemonSet] for [namespace: %s] = %d", namespace, len(daemonSets))
-
-	retSets := []apps_v1.DaemonSet{}
-	for _, ds := range daemonSets {
-		ds.Kind = kubernetes.DaemonSetType
-		retSets = append(retSets, *ds)
-	}
-	return retSets, nil
-}
-
-func (c *kialiCacheImpl) GetDaemonSet(namespace, name string) (*apps_v1.DaemonSet, error) {
-	log.Tracef("[Kiali Cache] Get [resource: DaemonSet] for [namespace: %s] [name: %s]", namespace, name)
-	ds, err := c.getCacheLister(namespace).daemonSetLister.DaemonSets(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	ds.Kind = kubernetes.DaemonSetType
-	return c.getCacheLister(namespace).daemonSetLister.DaemonSets(namespace).Get(name)
-}
-
-func (c *kialiCacheImpl) GetDeployments(namespace string) ([]apps_v1.Deployment, error) {
-	deployments, err := c.getCacheLister(namespace).deploymentLister.Deployments(namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-	log.Tracef("[Kiali Cache] Get [resource: Deployment] for [namespace: %s] = %d", namespace, len(deployments))
-
-	retDeployments := []apps_v1.Deployment{}
-	for _, deployment := range deployments {
-		deployment.Kind = kubernetes.DeploymentType
-		retDeployments = append(retDeployments, *deployment)
-	}
-	return retDeployments, nil
-}
-
-func (c *kialiCacheImpl) GetDeployment(namespace, name string) (*apps_v1.Deployment, error) {
-	log.Tracef("[Kiali Cache] Get [resource: Deployment] for [namespace: %s] [name: %s]", namespace, name)
-	deployment, err := c.getCacheLister(namespace).deploymentLister.Deployments(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	deployment.Kind = kubernetes.DeploymentType
-	return deployment, nil
-}
-
-func (c *kialiCacheImpl) GetEndpoints(namespace, name string) (*core_v1.Endpoints, error) {
-	log.Tracef("[Kiali Cache] Get [resource: Endpoints] for [namespace: %s] [name: %s]", namespace, name)
-	endpoints, err := c.getCacheLister(namespace).endpointLister.Endpoints(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	endpoints.Kind = kubernetes.EndpointsType
-	return endpoints, nil
-}
-
-func (c *kialiCacheImpl) GetStatefulSets(namespace string) ([]apps_v1.StatefulSet, error) {
-	statefulSets, err := c.getCacheLister(namespace).statefulSetLister.StatefulSets(namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-	log.Tracef("[Kiali Cache] Get [resource: StatefulSet] for [namespace: %s] = %d", namespace, len(statefulSets))
-
-	retSets := []apps_v1.StatefulSet{}
-	for _, ss := range statefulSets {
-		ss.Kind = kubernetes.StatefulSetType
-		retSets = append(retSets, *ss)
-	}
-	return retSets, nil
-}
-
-func (c *kialiCacheImpl) GetStatefulSet(namespace, name string) (*apps_v1.StatefulSet, error) {
-	log.Tracef("[Kiali Cache] Get [resource: StatefulSet] for [namespace: %s] [name: %s]", namespace, name)
-	statefulSet, err := c.getCacheLister(namespace).statefulSetLister.StatefulSets(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	statefulSet.Kind = kubernetes.StatefulSetType
-	return statefulSet, nil
-}
-
-func (c *kialiCacheImpl) GetServices(namespace string, selectorLabels map[string]string) ([]core_v1.Service, error) {
-	services, err := c.getCacheLister(namespace).serviceLister.Services(namespace).List(labels.Set(selectorLabels).AsSelector())
-	if err != nil {
-		return nil, err
-	}
-	log.Tracef("[Kiali Cache] Get [resource: Service] for [namespace: %s] = %d", namespace, len(services))
-
-	retServices := []core_v1.Service{}
-	for _, service := range services {
-		service.Kind = kubernetes.ServiceType
-		retServices = append(retServices, *service)
-	}
-	return retServices, nil
-}
-
-func (c *kialiCacheImpl) GetService(namespace, name string) (*core_v1.Service, error) {
-	log.Tracef("[Kiali Cache] Get [resource: Service] for [namespace: %s] [name: %s]", namespace, name)
-	service, err := c.getCacheLister(namespace).serviceLister.Services(namespace).Get(name)
-	if err != nil {
-		return nil, err
-	}
-
-	service.Kind = kubernetes.ServiceType
-	return service, nil
-}
-
-func (c *kialiCacheImpl) GetPods(namespace, labelSelector string) ([]core_v1.Pod, error) {
-	selector, err := labels.Parse(labelSelector)
-	if err != nil {
-		return nil, err
-	}
-
-	pods, err := c.getCacheLister(namespace).podLister.Pods(namespace).List(selector)
-	if err != nil {
-		return nil, err
-	}
-	log.Tracef("[Kiali Cache] Get [resource: Pod] for [namespace: %s] = %d", namespace, len(pods))
-
-	retPods := []core_v1.Pod{}
-	for _, pod := range pods {
-		pod.Kind = kubernetes.PodType
-		retPods = append(retPods, *pod)
-	}
-	return retPods, nil
-}
-
-// GetReplicaSets returns the cached ReplicaSets for the namespace.  For any given RS for a given
-// Owner (i.e. Deployment), only the most recent version of the RS will be included in the returned list.
-// When an owning Deployment is configured with revisionHistoryLimit > 0, then k8s may return multiple
-// versions of the RS for the same Deployment (current and older revisions). Note that it is still possible
-// to have multiple RS for the same owner. In which case the most recent version of each is returned.
-// see also: ../kubernetes.go
-func (c *kialiCacheImpl) GetReplicaSets(namespace string) ([]apps_v1.ReplicaSet, error) {
-	reps, err := c.getCacheLister(namespace).replicaSetLister.ReplicaSets(namespace).List(labels.Everything())
-	if err != nil {
-		return nil, err
-	}
-
-	result := []apps_v1.ReplicaSet{}
-	if len(reps) > 0 {
-		activeRSMap := map[string]*apps_v1.ReplicaSet{}
-		for _, rs := range reps {
-			if len(rs.OwnerReferences) > 0 {
-				for _, ownerRef := range rs.OwnerReferences {
-					if ownerRef.Controller != nil && *ownerRef.Controller {
-						key := fmt.Sprintf("%s_%s_%s", ownerRef.Name, rs.Name, rs.ResourceVersion)
-						if currRS, ok := activeRSMap[key]; ok {
-							if currRS.CreationTimestamp.Time.Before(rs.CreationTimestamp.Time) {
-								activeRSMap[key] = rs
-							}
-						} else {
-							activeRSMap[key] = rs
-						}
-					}
-				}
-			} else {
-				// it is it's own controller
-				activeRSMap[rs.Name] = rs
-			}
-		}
-
-		lenRS := len(activeRSMap)
-		result = make([]apps_v1.ReplicaSet, lenRS)
-		i := 0
-		for _, activeRS := range activeRSMap {
-			activeRS.Kind = kubernetes.ReplicaSetType
-			result[i] = *(activeRS)
-			i = i + 1
-		}
-		log.Tracef("[Kiali Cache] Get [resource: ReplicaSet] for [namespace: %s] = %d", namespace, lenRS)
-	}
-	return result, nil
-}
diff --git a/kubernetes/cache/proxy_status.go b/kubernetes/cache/proxy_status.go
index 34ba300f3..83028d564 100644
--- a/kubernetes/cache/proxy_status.go
+++ b/kubernetes/cache/proxy_status.go
@@ -1,31 +1,63 @@
 package cache
 
 import (
+	"context"
 	"strings"
 	"time"
 
+	"k8s.io/apimachinery/pkg/util/wait"
+
 	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/log"
 )
 
-type (
-	ProxyStatusCache interface {
-		CheckProxyStatus() bool
-		GetPodProxyStatus(namespace, pod string) *kubernetes.ProxyStatus
-		SetProxyStatus(proxyStatus []*kubernetes.ProxyStatus)
-		RefreshProxyStatus()
-	}
-)
+type ProxyStatusCache interface {
+	GetPodProxyStatus(namespace, pod string) *kubernetes.ProxyStatus
+}
 
-func (c *kialiCacheImpl) CheckProxyStatus() bool {
-	defer c.proxyStatusLock.RUnlock()
-	c.proxyStatusLock.RLock()
-	if c.proxyStatusCreated == nil {
-		return false
-	}
-	if time.Since(*c.proxyStatusCreated) > c.tokenNamespaceDuration {
-		return false
-	}
-	return true
+// pollIstiodForProxyStatus is a long running goroutine that will periodically poll istiod for proxy status.
+// Polling stops when the stopCacheChan is closed.
+func (c *kialiCacheImpl) pollIstiodForProxyStatus(ctx context.Context) {
+	log.Debug("[Kiali Cache] Starting polling istiod for proxy status")
+	go func() {
+		for {
+			select {
+			case <-ctx.Done():
+				log.Debug("[Kiali Cache] Stopping polling for istiod proxy status")
+				return
+			case <-time.After(c.tokenNamespaceDuration):
+				// Get the proxy status from istiod with some retries.
+				// Wrapping this so we can defer cancel.
+				func() {
+					ctx, cancel := context.WithTimeout(ctx, c.tokenNamespaceDuration)
+					defer cancel()
+
+					var (
+						proxyStatus []*kubernetes.ProxyStatus
+						err         error
+					)
+
+					interval := c.tokenNamespaceDuration / 2
+					retryErr := wait.PollImmediateUntilWithContext(ctx, interval, func(ctx context.Context) (bool, error) {
+						log.Trace("Getting proxy status from istiod")
+						proxyStatus, err = c.clientFactory.GetSAHomeClusterClient().GetProxyStatus()
+						if err != nil {
+							// TODO: Error checking could be done here to determine retry if GetProxyStatus provided that info.
+							return false, nil
+						}
+
+						return true, nil
+					})
+					if retryErr != nil {
+						log.Warningf("Error getting proxy status from istiod. Proxy status may be stale. Err: %v", err)
+						return
+					}
+
+					c.setProxyStatus(proxyStatus)
+				}()
+			}
+		}
+	}()
 }
 
 func (c *kialiCacheImpl) GetPodProxyStatus(namespace, pod string) *kubernetes.ProxyStatus {
@@ -39,12 +71,10 @@ func (c *kialiCacheImpl) GetPodProxyStatus(namespace, pod string) *kubernetes.Pr
 	return nil
 }
 
-func (c *kialiCacheImpl) SetProxyStatus(proxyStatus []*kubernetes.ProxyStatus) {
+func (c *kialiCacheImpl) setProxyStatus(proxyStatus []*kubernetes.ProxyStatus) {
 	defer c.proxyStatusLock.Unlock()
 	c.proxyStatusLock.Lock()
 	if len(proxyStatus) > 0 {
-		timeNow := time.Now()
-		c.proxyStatusCreated = &timeNow
 		for _, ps := range proxyStatus {
 			if ps != nil {
 				// Expected format <pod-name>.<namespace>
@@ -66,9 +96,3 @@ func (c *kialiCacheImpl) SetProxyStatus(proxyStatus []*kubernetes.ProxyStatus) {
 		}
 	}
 }
-
-func (c *kialiCacheImpl) RefreshProxyStatus() {
-	defer c.proxyStatusLock.Unlock()
-	c.proxyStatusLock.Lock()
-	c.proxyStatusNamespaces = make(map[string]map[string]podProxyStatus)
-}
diff --git a/kubernetes/client.go b/kubernetes/client.go
index 0d57585a1..4aa6a50e9 100644
--- a/kubernetes/client.go
+++ b/kubernetes/client.go
@@ -51,7 +51,7 @@ type K8SClient struct {
 	ClientInterface
 	token          string
 	k8s            kube.Interface
-	istioClientset *istio.Clientset
+	istioClientset istio.Interface
 	// Used in REST queries after bump to client-go v0.20.x
 	ctx context.Context
 	// isOpenShift private variable will check if kiali is deployed under an OpenShift cluster or not
@@ -90,7 +90,7 @@ func UseRemoteCreds(remoteSecret *RemoteSecret) (*rest.Config, error) {
 
 	serverParse := strings.Split(remoteSecret.Clusters[0].Cluster.Server, ":")
 	if len(serverParse) != 3 && len(serverParse) != 2 {
-		return nil, errors.New("Invalid remote API server URL")
+		return nil, errors.New("invalid remote API server URL")
 	}
 	host := strings.TrimPrefix(serverParse[1], "//")
 
@@ -100,7 +100,7 @@ func UseRemoteCreds(remoteSecret *RemoteSecret) (*rest.Config, error) {
 	}
 
 	if !strings.EqualFold(serverParse[0], "https") {
-		return nil, errors.New("Only HTTPS protocol is allowed in remote API server URL")
+		return nil, errors.New("only HTTPS protocol is allowed in remote API server URL")
 	}
 
 	// There's no need to add the BearerToken because it's ignored later on
@@ -112,7 +112,7 @@ func UseRemoteCreds(remoteSecret *RemoteSecret) (*rest.Config, error) {
 
 // ConfigClient return a client with the correct configuration
 // Returns configuration if Kiali is in Cluster when InCluster is true
-// Returns configuration if Kiali is not int Cluster when InCluster is false
+// Returns configuration if Kiali is not in Cluster when InCluster is false
 // It returns an error on any problem
 func ConfigClient() (*rest.Config, error) {
 	if kialiConfig.Get().InCluster {
@@ -172,5 +172,15 @@ func NewClientFromConfig(config *rest.Config) (*K8SClient, error) {
 	}
 
 	client.ctx = context.Background()
+
 	return &client, nil
 }
+
+// NewClient is just used for testing purposes.
+func NewClient(kubeClient kube.Interface, istioClient istio.Interface, gatewayapiClient gatewayapiclient.Interface) *K8SClient {
+	return &K8SClient{
+		istioClientset: istioClient,
+		k8s:            kubeClient,
+		gatewayapi:     gatewayapiClient,
+	}
+}
diff --git a/kubernetes/client_factory.go b/kubernetes/client_factory.go
index 1ab7bdc63..6642552db 100644
--- a/kubernetes/client_factory.go
+++ b/kubernetes/client_factory.go
@@ -18,19 +18,26 @@ var factory *clientFactory
 // defaultExpirationTime set the default expired time of a client
 const defaultExpirationTime = time.Minute * 15
 
+const HomeClusterName = "_kiali_home"
+
 // ClientFactory interface for the clientFactory object
 type ClientFactory interface {
 	GetClient(authInfo *api.AuthInfo) (ClientInterface, error)
+	GetSAClients() map[string]ClientInterface
+	GetSAHomeClusterClient() ClientInterface
 }
 
 // clientFactory used to generate per users clients
 type clientFactory struct {
 	baseIstioConfig *rest.Config
-	clientEntries   map[string]*ClientInterface
+	clientEntries   map[string]ClientInterface
 	// mutex for when modifying the stored clients
 	mutex sync.RWMutex
 	// when a client is expired, a signal with its tokenHash will be sent to recycleChan
 	recycleChan chan string
+	// maps cluster name to a kiali client for that cluster. The kiali client uses the
+	// kiali service account to access the cluster API.
+	saClientEntries map[string]ClientInterface
 }
 
 // GetClientFactory returns the client factory. Creates a new one if necessary
@@ -50,28 +57,43 @@ func GetClientFactory() (ClientFactory, error) {
 			Burst:           config.Burst,
 		}
 
-		factory = newClientFactory(&istioConfig)
+		cf, err := newClientFactory(&istioConfig)
+		if err != nil {
+			return nil, err
+		}
+
+		factory = cf
 	}
 	return factory, nil
 }
 
 // newClientFactory allows for specifying the config and expiry duration
 // Mock friendly for testing purposes
-func newClientFactory(istioConfig *rest.Config) *clientFactory {
-	clientEntriesMap := make(map[string]*ClientInterface)
-
+func newClientFactory(istioConfig *rest.Config) (*clientFactory, error) {
 	f := &clientFactory{
 		baseIstioConfig: istioConfig,
-		clientEntries:   clientEntriesMap,
+		clientEntries:   make(map[string]ClientInterface),
 		recycleChan:     make(chan string),
+		saClientEntries: make(map[string]ClientInterface),
 	}
-
 	// after creating a client factory
 	// background goroutines will be watching the clients` expiration
 	// if a client is expired, it will be removed from clientEntries
 	go f.watchClients()
 
-	return f
+	// Create serivce account clients.
+	// TODO: Create a service account client for each cluster. Need a way to get all configured clusters.
+	// TODO: Use a real cluster name instead of "home"
+	clusters := []string{HomeClusterName}
+	for _, cluster := range clusters {
+		client, err := f.newSAClient(cluster)
+		if err != nil {
+			return nil, err
+		}
+		f.saClientEntries[cluster] = client
+	}
+
+	return f, nil
 }
 
 // newClient creates a new ClientInterface based on a users k8s token
@@ -144,22 +166,35 @@ func (cf *clientFactory) newClient(authInfo *api.AuthInfo, expirationTime time.D
 	return newClient, err
 }
 
+func (cf *clientFactory) newSAClient(cluster string) (*K8SClient, error) {
+	// TODO: Need a way to load in cluster configuration from the kube secret.
+	if kialiConfig.Get().InCluster {
+		if saToken, err := GetKialiToken(); err != nil {
+			return nil, err
+		} else {
+			cf.baseIstioConfig.BearerToken = saToken
+		}
+	}
+
+	return NewClientFromConfig(cf.baseIstioConfig)
+}
+
 // GetClient returns a client for the specified token. Creating one if necessary.
 func (cf *clientFactory) GetClient(authInfo *api.AuthInfo) (ClientInterface, error) {
 	client, err := cf.getClient(authInfo)
 	if err != nil {
 		return nil, err
 	}
-	return *client, nil
+	return client, nil
 }
 
 // getClient returns a client for the specified token. Creating one if necessary.
-func (cf *clientFactory) getClient(authInfo *api.AuthInfo) (*ClientInterface, error) {
+func (cf *clientFactory) getClient(authInfo *api.AuthInfo) (ClientInterface, error) {
 	return cf.getRecycleClient(authInfo, defaultExpirationTime)
 }
 
 // getRecycleClient returns a client for the specified token with expirationTime. Creating one if necessary.
-func (cf *clientFactory) getRecycleClient(authInfo *api.AuthInfo, expirationTime time.Duration) (*ClientInterface, error) {
+func (cf *clientFactory) getRecycleClient(authInfo *api.AuthInfo, expirationTime time.Duration) (ClientInterface, error) {
 	cf.mutex.Lock()
 	defer cf.mutex.Unlock()
 	tokenHash := getTokenHash(authInfo)
@@ -172,16 +207,16 @@ func (cf *clientFactory) getRecycleClient(authInfo *api.AuthInfo, expirationTime
 			return nil, err
 		}
 
-		cf.clientEntries[getTokenHash(authInfo)] = &client
+		cf.clientEntries[getTokenHash(authInfo)] = client
 		internalmetrics.SetKubernetesClients(len(cf.clientEntries))
-		return &client, nil
+		return client, nil
 	}
 }
 
 // hasClient check if clientFactory has a client, return the client if clientFactory has it
 // This is a helper function for testing.
 // It uses the shared lock so beware of nested locking with other methods.
-func (cf *clientFactory) hasClient(authInfo *api.AuthInfo) (*ClientInterface, bool) {
+func (cf *clientFactory) hasClient(authInfo *api.AuthInfo) (ClientInterface, bool) {
 	tokenHash := getTokenHash(authInfo)
 	cf.mutex.RLock()
 	cEntry, ok := cf.clientEntries[tokenHash]
@@ -253,3 +288,53 @@ func getTokenHash(authInfo *api.AuthInfo) string {
 	}
 	return string(h.Sum(nil))
 }
+
+// KialiSAClients returns all clients associated with the Kiali service account across clusters.
+func (cf *clientFactory) GetSAClients() map[string]ClientInterface {
+	cf.mutex.RLock()
+	defer cf.mutex.RUnlock()
+
+	for cluster := range cf.saClientEntries {
+		if err := cf.refreshClientIfTokenChanged(cluster); err != nil {
+			log.Errorf("Unable to refresh Kiali SA client for cluster: %s. Err: %s", cluster, err)
+		}
+	}
+
+	return cf.saClientEntries
+}
+
+// Check for kiali token changes and refresh the client when it does.
+func (cf *clientFactory) refreshClientIfTokenChanged(cluster string) error {
+	// TODO: This only checks that the token has changed. When the client factory supports reading
+	// the tokens from the filesystem, then this should check the client against the token on disk
+	// that is associated with the cluster.
+	kialiSAToken, err := GetKialiToken()
+	if err != nil {
+		return err
+	}
+
+	if cf.saClientEntries[cluster].GetToken() != kialiSAToken {
+		log.Debugf("Kiali SA token has changed, refreshing client for cluster: %s", cluster)
+		// Token has changed, so we need to refresh the client.
+		newClient, err := cf.newSAClient(cluster)
+		if err != nil {
+			return err
+		}
+		cf.saClientEntries[cluster] = newClient
+	}
+
+	return nil
+}
+
+// KialiSAHomeCluster returns the Kiali service account client for the cluster where Kiali is running.
+func (cf *clientFactory) GetSAHomeClusterClient() ClientInterface {
+	cf.mutex.RLock()
+	defer cf.mutex.RUnlock()
+
+	if err := cf.refreshClientIfTokenChanged(HomeClusterName); err != nil {
+		log.Errorf("Unable to refresh Kiali SA client for home cluster. Err: %s", err)
+	}
+
+	// TODO: Use a real cluster name instead of "home"
+	return cf.saClientEntries[HomeClusterName]
+}
diff --git a/kubernetes/filters.go b/kubernetes/filters.go
index fbc393a99..78ef980d5 100644
--- a/kubernetes/filters.go
+++ b/kubernetes/filters.go
@@ -9,7 +9,7 @@ import (
 	security_v1beta1 "istio.io/client-go/pkg/apis/security/v1beta1"
 	core_v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
-	k8s_networking_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	"github.com/kiali/kiali/config"
 )
@@ -160,12 +160,12 @@ func FilterSupportedGateways(gateways []*networking_v1beta1.Gateway) []*networki
 	return filtered
 }
 
-func FilterSupportedK8sGateways(gateways []*k8s_networking_v1alpha2.Gateway) []*k8s_networking_v1alpha2.Gateway {
+func FilterSupportedK8sGateways(gateways []*k8s_networking_v1beta1.Gateway) []*k8s_networking_v1beta1.Gateway {
 	var gatewayAPIClassName = config.Get().ExternalServices.Istio.GatewayAPIClassName
 	if gatewayAPIClassName == "" {
 		gatewayAPIClassName = "istio"
 	}
-	filtered := []*k8s_networking_v1alpha2.Gateway{}
+	filtered := []*k8s_networking_v1beta1.Gateway{}
 	for _, gw := range gateways {
 		if string(gw.Spec.GatewayClassName) == gatewayAPIClassName {
 			filtered = append(filtered, gw)
@@ -219,13 +219,13 @@ func FilterGatewaysByVirtualServices(allGws []*networking_v1beta1.Gateway, allVs
 	return gateways
 }
 
-func FilterK8sGatewaysByHTTPRoutes(allGws []*k8s_networking_v1alpha2.Gateway, allRoutes []*k8s_networking_v1alpha2.HTTPRoute) []*k8s_networking_v1alpha2.Gateway {
+func FilterK8sGatewaysByHTTPRoutes(allGws []*k8s_networking_v1beta1.Gateway, allRoutes []*k8s_networking_v1beta1.HTTPRoute) []*k8s_networking_v1beta1.Gateway {
 	var empty struct{}
-	gateways := []*k8s_networking_v1alpha2.Gateway{}
+	gateways := []*k8s_networking_v1beta1.Gateway{}
 	gatewayNames := make(map[string]struct{})
 	for _, route := range allRoutes {
 		for _, pRef := range route.Spec.ParentRefs {
-			if *pRef.Namespace != "" {
+			if pRef.Namespace != nil && *pRef.Namespace != "" {
 				gatewayNames[fmt.Sprintf("%s/%s", *pRef.Namespace, pRef.Name)] = empty
 			} else {
 				gatewayNames[fmt.Sprintf("%s/%s", route.Namespace, pRef.Name)] = empty
@@ -530,8 +530,8 @@ func FilterVirtualServicesByService(allVs []*networking_v1beta1.VirtualService,
 	return filtered
 }
 
-func FilterK8sHTTPRoutesByService(allRoutes []*k8s_networking_v1alpha2.HTTPRoute, namespace string, serviceName string) []*k8s_networking_v1alpha2.HTTPRoute {
-	filtered := []*k8s_networking_v1alpha2.HTTPRoute{}
+func FilterK8sHTTPRoutesByService(allRoutes []*k8s_networking_v1beta1.HTTPRoute, namespace string, serviceName string) []*k8s_networking_v1beta1.HTTPRoute {
+	filtered := []*k8s_networking_v1beta1.HTTPRoute{}
 	for _, route := range allRoutes {
 		appendRoute := serviceName == ""
 		if !appendRoute {
diff --git a/kubernetes/istio.go b/kubernetes/istio.go
index 776861656..1c562035b 100644
--- a/kubernetes/istio.go
+++ b/kubernetes/istio.go
@@ -23,7 +23,7 @@ import (
 	istio "istio.io/client-go/pkg/clientset/versioned"
 	core_v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
-	k8s_networking_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 	gatewayapiclient "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
 
 	"github.com/kiali/kiali/config"
@@ -456,8 +456,8 @@ func ParseRegistryConfig(config map[string][]byte) (*RegistryConfiguration, erro
 		Telemetries:      []*v1alpha1.Telemetry{},
 
 		// K8s Networking Gateways
-		K8sGateways:   []*k8s_networking_v1alpha2.Gateway{},
-		K8sHTTPRoutes: []*k8s_networking_v1alpha2.HTTPRoute{},
+		K8sGateways:   []*k8s_networking_v1beta1.Gateway{},
+		K8sHTTPRoutes: []*k8s_networking_v1beta1.HTTPRoute{},
 
 		AuthorizationPolicies:  []*security_v1beta1.AuthorizationPolicy{},
 		PeerAuthentications:    []*security_v1beta1.PeerAuthentication{},
@@ -508,7 +508,7 @@ func ParseRegistryConfig(config map[string][]byte) (*RegistryConfiguration, erro
 						case "Gateway":
 							// It needs to figure out Gateway object type by apiVersion, whether it is Gateway API of Istio Gateway
 							if mItem["apiVersion"] == K8sApiNetworkingVersionV1Beta1 || mItem["apiVersion"] == K8sApiNetworkingVersionV1Alpha2 {
-								var gw k8s_networking_v1alpha2.Gateway
+								var gw k8s_networking_v1beta1.Gateway
 								err := bDec.Decode(&gw)
 								if err != nil {
 									log.Errorf("Error parsing RegistryConfig results for K8sGateways: %s", err)
@@ -523,7 +523,7 @@ func ParseRegistryConfig(config map[string][]byte) (*RegistryConfiguration, erro
 								registry.Gateways = append(registry.Gateways, &gw)
 							}
 						case "HTTPRoute":
-							var route *k8s_networking_v1alpha2.HTTPRoute
+							var route *k8s_networking_v1beta1.HTTPRoute
 							err := bDec.Decode(&route)
 							if err != nil {
 								log.Errorf("Error parsing RegistryConfig results for K8sHTTPRoutes: %s", err)
@@ -790,6 +790,16 @@ func GatewayNames(gateways []*networking_v1beta1.Gateway) map[string]struct{} {
 	return names
 }
 
+// K8sGatewayNames extracts the gateway names for easier matching
+func K8sGatewayNames(gateways []*k8s_networking_v1beta1.Gateway) map[string]struct{} {
+	var empty struct{}
+	names := make(map[string]struct{})
+	for _, gw := range gateways {
+		names[ParseHost(gw.Name, gw.Namespace).String()] = empty
+	}
+	return names
+}
+
 func PeerAuthnHasStrictMTLS(peerAuthn *security_v1beta1.PeerAuthentication) bool {
 	_, mode := PeerAuthnHasMTLSEnabled(peerAuthn)
 	return mode == "STRICT"
diff --git a/kubernetes/kubernetes.go b/kubernetes/kubernetes.go
index b452a6fde..9792d94bd 100644
--- a/kubernetes/kubernetes.go
+++ b/kubernetes/kubernetes.go
@@ -25,6 +25,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/version"
+	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/tools/clientcmd/api"
 
@@ -35,6 +36,8 @@ import (
 )
 
 type K8SClientInterface interface {
+	// Kube returns the underlying kubernetes client.
+	Kube() kubernetes.Interface
 	GetClusterServicesByLabels(labelsSelector string) ([]core_v1.Service, error)
 	GetConfigMap(namespace, name string) (*core_v1.ConfigMap, error)
 	GetCronJobs(namespace string) ([]batch_v1.CronJob, error)
@@ -100,6 +103,10 @@ func (in *K8SClient) forwardGetRequest(namespace, podName string, destinationPor
 	return resp, err
 }
 
+func (in *K8SClient) Kube() kubernetes.Interface {
+	return in.k8s
+}
+
 // GetClusterServicesByLabels fetches and returns all services in the whole cluster
 // that match the optional labelSelector. This is using the cluster-wide call
 // to fetch the services. The client will need to be created with an account that
@@ -442,7 +449,7 @@ func (in *K8SClient) getPodPortForwarder(namespace, name, portMap string) (httpu
 
 	// Create a Port Forwarder
 	restInterface := in.k8s.CoreV1().RESTClient()
-	return httputil.NewPortForwarder(&restInterface, clientConfig,
+	return httputil.NewPortForwarder(restInterface, clientConfig,
 		namespace, name, "localhost", portMap, writer)
 }
 
diff --git a/kubernetes/types.go b/kubernetes/types.go
index 22000520a..3346b3f30 100644
--- a/kubernetes/types.go
+++ b/kubernetes/types.go
@@ -8,9 +8,9 @@ import (
 	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
 	security_v1beta "istio.io/client-go/pkg/apis/security/v1beta1"
 	"istio.io/client-go/pkg/apis/telemetry/v1alpha1"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	k8s_networking_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 )
 
 const (
@@ -163,8 +163,8 @@ var (
 		WasmPlugins:      ExtensionGroupVersionV1Alpha1.Group,
 		Telemetries:      TelemetryGroupV1Alpha1.Group,
 
-		K8sGateways:   K8sNetworkingGroupVersionV1Alpha2.Group,
-		K8sHTTPRoutes: K8sNetworkingGroupVersionV1Alpha2.Group,
+		K8sGateways:   K8sNetworkingGroupVersionV1Beta1.Group,
+		K8sHTTPRoutes: K8sNetworkingGroupVersionV1Beta1.Group,
 
 		AuthorizationPolicies:  SecurityGroupVersion.Group,
 		PeerAuthentications:    SecurityGroupVersion.Group,
@@ -226,8 +226,8 @@ type RegistryConfiguration struct {
 	Telemetries      []*v1alpha1.Telemetry
 
 	// K8s Networking Gateways
-	K8sGateways   []*k8s_networking_v1alpha2.Gateway
-	K8sHTTPRoutes []*k8s_networking_v1alpha2.HTTPRoute
+	K8sGateways   []*k8s_networking_v1beta1.Gateway
+	K8sHTTPRoutes []*k8s_networking_v1beta1.HTTPRoute
 
 	// Security
 	AuthorizationPolicies  []*security_v1beta.AuthorizationPolicy
diff --git a/log/log.go b/log/log.go
index 4ccaf77b2..d657d670f 100644
--- a/log/log.go
+++ b/log/log.go
@@ -118,6 +118,12 @@ func Fatalf(format string, args ...interface{}) {
 	log.Fatal().Msgf(format, args...)
 }
 
+// Return log level
+// Used to get debug info
+func GetLogLevel() string {
+	return log.Logger.GetLevel().String()
+}
+
 // Resolves the environment settings for the log level. Considers the verbose_mode from server version <=1.25.
 func resolveLogLevelFromEnv() zerolog.Level {
 	logLevel, isDefined := os.LookupEnv("LOG_LEVEL")
diff --git a/make/Makefile.olm.mk b/make/Makefile.olm.mk
index 5aaef5fd5..2b3c1fa74 100644
--- a/make/Makefile.olm.mk
+++ b/make/Makefile.olm.mk
@@ -10,8 +10,8 @@ OLM_INDEX_NAME ?= ${OLM_IMAGE_ORG}/kiali-operator-index
 # set this package name to kiali-ossm if you want to test with the OSSM metadata
 OLM_BUNDLE_PACKAGE ?= kiali
 
-OLM_INDEX_BASE_IMAGE ?= quay.io/openshift/origin-operator-registry:4.10
-OPM_VERSION ?= 1.22.1
+OLM_INDEX_BASE_IMAGE ?= quay.io/openshift/origin-operator-registry:4.11
+OPM_VERSION ?= 1.25.0
 
 # which OLM to install (for olm-install target)
 OLM_VERSION ?= latest
diff --git a/make/Makefile.operator.mk b/make/Makefile.operator.mk
index c952a4027..80457260d 100644
--- a/make/Makefile.operator.mk
+++ b/make/Makefile.operator.mk
@@ -133,7 +133,7 @@ endif
 	@if [ ! -z "${ANSIBLE_PYTHON_INTERPRETER}" ]; then echo "ANSIBLE_PYTHON_INTERPRETER is [${ANSIBLE_PYTHON_INTERPRETER}]. Make sure that refers to a Python3 installation. If you do not have Python3 in that location, you must ensure you have Python3 and ANSIBLE_PYTHON_INTERPRETER is set to '-e ansible_python_interpreter=<full path to your python3 executable>"; fi
 	@echo "Ensure the CRDs exist"; ${OC} apply -f ${HELM_CHARTS_REPO}/kiali-operator/crds/crds.yaml
 	@echo "Create a dummy Kiali CR"; ${OC} apply -f ${ROOTDIR}/operator/dev-playbook-config/dev-kiali-cr.yaml
-	ansible-galaxy collection install operator_sdk.util community.kubernetes
+	ansible-galaxy collection install operator_sdk.util kubernetes.core
 	ALLOW_AD_HOC_KIALI_NAMESPACE=true ALLOW_AD_HOC_KIALI_IMAGE=true ALLOW_ALL_ACCESSIBLE_NAMESPACES=true ANSIBLE_ROLES_PATH=${ROOTDIR}/operator/roles ${ANSIBLE_CALLBACK_WHITELIST_ARG} ansible-playbook -vvv ${ANSIBLE_PYTHON_INTERPRETER} -i ${ROOTDIR}/operator/dev-playbook-config/dev-hosts.yaml ${ROOTDIR}/operator/dev-playbook-config/dev-playbook.yaml
 	@echo "Remove the dummy Kiali CR"; ${OC} delete -f ${ROOTDIR}/operator/dev-playbook-config/dev-kiali-cr.yaml
 
diff --git a/models/istio_config.go b/models/istio_config.go
index e98601462..6bd6f901f 100644
--- a/models/istio_config.go
+++ b/models/istio_config.go
@@ -6,7 +6,7 @@ import (
 	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
 	security_v1beta "istio.io/client-go/pkg/apis/security/v1beta1"
 	"istio.io/client-go/pkg/apis/telemetry/v1alpha1"
-	k8s_networking_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 )
 
 // IstioConfigList istioConfigList
@@ -29,8 +29,8 @@ type IstioConfigList struct {
 	WasmPlugins      []*extentions_v1alpha1.WasmPlugin     `json:"wasmPlugins"`
 	Telemetries      []*v1alpha1.Telemetry                 `json:"telemetries"`
 
-	K8sGateways   []*k8s_networking_v1alpha2.Gateway   `json:"k8sGateways"`
-	K8sHTTPRoutes []*k8s_networking_v1alpha2.HTTPRoute `json:"k8sHTTPRoutes"`
+	K8sGateways   []*k8s_networking_v1beta1.Gateway   `json:"k8sGateways"`
+	K8sHTTPRoutes []*k8s_networking_v1beta1.HTTPRoute `json:"k8sHTTPRoutes"`
 
 	AuthorizationPolicies  []*security_v1beta.AuthorizationPolicy   `json:"authorizationPolicies"`
 	PeerAuthentications    []*security_v1beta.PeerAuthentication    `json:"peerAuthentications"`
@@ -56,8 +56,8 @@ type IstioConfigDetails struct {
 	WasmPlugin            *extentions_v1alpha1.WasmPlugin        `json:"wasmPlugin"`
 	Telemetry             *v1alpha1.Telemetry                    `json:"telemetry"`
 
-	K8sGateway   *k8s_networking_v1alpha2.Gateway   `json:"k8sGateway"`
-	K8sHTTPRoute *k8s_networking_v1alpha2.HTTPRoute `json:"k8sHTTPRoute"`
+	K8sGateway   *k8s_networking_v1beta1.Gateway   `json:"k8sGateway"`
+	K8sHTTPRoute *k8s_networking_v1beta1.HTTPRoute `json:"k8sHTTPRoute"`
 
 	Permissions           ResourcePermissions `json:"permissions"`
 	IstioValidation       *IstioValidation    `json:"validation"`
@@ -159,8 +159,11 @@ var IstioConfigHelpMessages = map[string][]IstioConfigHelp{
 	"telemetries": { //TODO
 		{},
 	},
-	"k8sgateways": { //TODO
+	"k8sgateways": {
 		{ObjectField: "", Message: "Kubernetes Gateway API Configuration Object. A Gateway describes how traffic can be translated to Services within the cluster."},
+		{ObjectField: "spec.GatewayClassName", Message: "Defines the name of a GatewayClass object used by this Gateway."},
+		{ObjectField: "spec.listeners", Message: "Define the hostnames, ports, protocol, termination, TLS settings and which routes can be attached to a listener."},
+		{ObjectField: "spec.addresses", Message: "Define the network addresses requested for this gateway."},
 	},
 	"k8shttproutes": { //TODO
 		{ObjectField: "", Message: "Kubernetes Gateway API Configuration Object. HTTPRoute is for multiplexing HTTP or terminated HTTPS connections."},
@@ -199,8 +202,8 @@ func (configList IstioConfigList) FilterIstioConfigs(nss []string) *IstioConfigs
 			filtered[ns].DestinationRules = []*networking_v1beta1.DestinationRule{}
 			filtered[ns].EnvoyFilters = []*networking_v1alpha3.EnvoyFilter{}
 			filtered[ns].Gateways = []*networking_v1beta1.Gateway{}
-			filtered[ns].K8sGateways = []*k8s_networking_v1alpha2.Gateway{}
-			filtered[ns].K8sHTTPRoutes = []*k8s_networking_v1alpha2.HTTPRoute{}
+			filtered[ns].K8sGateways = []*k8s_networking_v1beta1.Gateway{}
+			filtered[ns].K8sHTTPRoutes = []*k8s_networking_v1beta1.HTTPRoute{}
 			filtered[ns].VirtualServices = []*networking_v1beta1.VirtualService{}
 			filtered[ns].ServiceEntries = []*networking_v1beta1.ServiceEntry{}
 			filtered[ns].Sidecars = []*networking_v1beta1.Sidecar{}
diff --git a/models/istio_validation.go b/models/istio_validation.go
index f720f208b..7a137c2e2 100644
--- a/models/istio_validation.go
+++ b/models/istio_validation.go
@@ -114,6 +114,8 @@ var ObjectTypeSingular = map[string]string{
 	"workloads":              "workload",
 	"wasmplugins":            "wasmpluin",
 	"telemetries":            "telemetry",
+	"k8shttproutes":          "k8shttproute",
+	"k8sgateways":            "k8sgateway",
 }
 
 var checkDescriptors = map[string]IstioCheck{
@@ -217,6 +219,16 @@ var checkDescriptors = map[string]IstioCheck{
 		Message:  "No matching workload found for the selector in this namespace",
 		Severity: WarningSeverity,
 	},
+	"k8shttproutes.nohost.namenotfound": {
+		Code:     "KIA1402",
+		Message:  "BackendRef on rule doesn't have a valid service (host not found)",
+		Severity: ErrorSeverity,
+	},
+	"k8shttproutes.nok8sgateway": {
+		Code:     "KIA1401",
+		Message:  "HTTPRoute is pointing to a non-existent K8s gateway",
+		Severity: ErrorSeverity,
+	},
 	"peerauthentication.mtls.destinationrulemissing": {
 		Code:     "KIA0401",
 		Message:  "Mesh-wide Destination Rule enabling mTLS is missing",
@@ -307,6 +319,21 @@ var checkDescriptors = map[string]IstioCheck{
 		Message:  "This workload is not covered by any authorization policy",
 		Severity: WarningSeverity,
 	},
+	"k8sgateways.multimatch.listener": {
+		Code:     "KIA1501",
+		Message:  "More than one K8s Gateway for the same host port combination",
+		Severity: WarningSeverity,
+	},
+	"k8sgateways.multimatch.ip": {
+		Code:     "KIA1502",
+		Message:  "More than one K8s Gateway for the same address and type combination",
+		Severity: WarningSeverity,
+	},
+	"k8sgateways.unique.listener": {
+		Code:     "KIA1503",
+		Message:  "Each listener must have a unique combination of Hostname, Port, and Protocol",
+		Severity: ErrorSeverity,
+	},
 }
 
 func Build(checkId string, path string) IstioCheck {
diff --git a/models/service.go b/models/service.go
index a10890223..98a710080 100644
--- a/models/service.go
+++ b/models/service.go
@@ -4,7 +4,7 @@ import (
 	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
 	core_v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
-	k8s_networking_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
@@ -68,7 +68,7 @@ type ServiceDetails struct {
 	Endpoints        Endpoints                             `json:"endpoints"`
 	VirtualServices  []*networking_v1beta1.VirtualService  `json:"virtualServices"`
 	DestinationRules []*networking_v1beta1.DestinationRule `json:"destinationRules"`
-	K8sHTTPRoutes    []*k8s_networking_v1alpha2.HTTPRoute  `json:"k8sHTTPRoutes"`
+	K8sHTTPRoutes    []*k8s_networking_v1beta1.HTTPRoute   `json:"k8sHTTPRoutes"`
 	ServiceEntries   []*networking_v1beta1.ServiceEntry    `json:"serviceEntries"`
 	IstioPermissions ResourcePermissions                   `json:"istioPermissions"`
 	Workloads        WorkloadOverviews                     `json:"workloads"`
diff --git a/models/workload.go b/models/workload.go
index da4f0999e..960f1dc45 100644
--- a/models/workload.go
+++ b/models/workload.go
@@ -11,6 +11,7 @@ import (
 	"k8s.io/apimachinery/pkg/labels"
 
 	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/status"
 )
 
 type WorkloadList struct {
@@ -176,24 +177,29 @@ func (workload *Workload) parseObjectMeta(meta *meta_v1.ObjectMeta, tplMeta *met
 		annotations = tplMeta.Annotations
 	}
 
-	// check for automatic sidecar injection at the workload level - can be defined via label or annotation
-	// if both are defined, either one set to false will disable injection
-	labelExplicitlyFalse := false // true means the label is defined and explicitly set to false
+	// Check for automatic sidecar injection config at the workload level. This can be defined via label or annotation.
+	// This code ignores any namespace injection label - this determines auto-injection config as defined by workload-only label or annotation.
+	// If both are defined, label always overrides annotation (see https://github.com/kiali/kiali/issues/5713)
+	// If none are defined, assume injection is disabled (again, we ignore the possibility of a namespace label enabling injection)
+	labelExplicitlySet := false // true means the label is defined
 	label, exist := workload.Labels[conf.ExternalServices.Istio.IstioInjectionAnnotation]
 	if exist {
 		if value, err := strconv.ParseBool(label); err == nil {
 			workload.IstioInjectionAnnotation = &value
-			labelExplicitlyFalse = !value
+			labelExplicitlySet = true
 		}
 	}
 
-	// do not bother to check the annotation if the label is explicitly false since we know in that case injection is disabled
-	if !labelExplicitlyFalse {
+	// do not bother to check the annotation if the label is explicitly set - label always overrides the annotation
+	if !labelExplicitlySet {
 		annotation, exist := annotations[conf.ExternalServices.Istio.IstioInjectionAnnotation]
 		if exist {
 			if value, err := strconv.ParseBool(annotation); err == nil {
 				if !value {
 					workload.IstioInjectionAnnotation = &value
+				} else if status.IsMaistra() {
+					// annotation value of true is only meaningful in a OSSM/Maistra environment
+					workload.IstioInjectionAnnotation = &value
 				}
 			}
 		}
diff --git a/routing/routes.go b/routing/routes.go
index 86a1c2f23..761114ea0 100644
--- a/routing/routes.go
+++ b/routing/routes.go
@@ -1430,7 +1430,7 @@ func NewRoutes() (r *Routes) {
 		//
 		// responses:
 		//              500: internalError
-		//              200: istiodResourceThreholds
+		//              200: istiodResourceThresholds
 		{
 			"IstiodResourceThresholds",
 			"GET",
diff --git a/status/status.go b/status/status.go
index ef160d66d..07591773e 100644
--- a/status/status.go
+++ b/status/status.go
@@ -28,6 +28,8 @@ type IstioEnvironment struct {
 	//
 	// required: true
 	IsMaistra bool `json:"isMaistra"`
+	// Is api enabled
+	IstioAPIEnabled bool `json:"istioAPIEnabled"`
 }
 
 // StatusInfo statusInfo
@@ -142,7 +144,8 @@ func Get() (status StatusInfo) {
 			}
 		}
 		info.IstioEnvironment = &IstioEnvironment{
-			IsMaistra: isMaistra,
+			IsMaistra:       isMaistra,
+			IstioAPIEnabled: cfg.ExternalServices.Istio.IstioAPIEnabled,
 		}
 	}
 
diff --git a/status/versions.go b/status/versions.go
index 654fdaba4..ecf1b0262 100644
--- a/status/versions.go
+++ b/status/versions.go
@@ -7,7 +7,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/hashicorp/go-version"
 	kversion "k8s.io/apimachinery/pkg/version"
 	kube "k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
@@ -86,158 +85,7 @@ func getVersionComponent(serviceComponent externalService) {
 	}
 }
 
-// validateVersion returns true if requiredVersion "<op> version" (e.g. ">= 0.7.1") is satisfied by installedversion
-func validateVersion(requiredVersion string, installedVersion string) bool {
-	reqWords := strings.Split(requiredVersion, " ")
-	requirementV, errReqV := version.NewVersion(reqWords[1])
-	installedV, errInsV := version.NewVersion(installedVersion)
-	if errReqV != nil || errInsV != nil {
-		return false
-	}
-	switch operator := reqWords[0]; operator {
-	case "==":
-		return installedV.Equal(requirementV)
-	case ">=":
-		return installedV.GreaterThan(requirementV) || installedV.Equal(requirementV)
-	case ">":
-		return installedV.GreaterThan(requirementV)
-	case "<=":
-		return installedV.LessThan(requirementV) || installedV.Equal(requirementV)
-	case "<":
-		return installedV.LessThan(requirementV)
-	}
-	return false
-}
-
-// CheckMeshVersion check mesh if its version is compatible with kiali
-func CheckMeshVersion(meshName string, meshVersion string, kialiVersion string) bool {
-	ok := false
-	if meshName == istioProductNameUnknown {
-		return ok
-	}
-	if strings.Contains(meshName, istioProductNameUpstream) {
-		ok = checkIstioVersion(meshVersion, kialiVersion)
-		return ok
-	} else if strings.Contains(meshName, istioProductNameOSSM) {
-		ok = checkOSSMVersion(meshVersion, kialiVersion)
-		return ok
-	}
-	return ok
-}
-
-// checkOSSMVersion check OpenShift Service Mesh if its version is compatible with kiali. There is a 1-to-1 relationship between compatible versions.
-// So there is no range checking. The kiali fixed version is checked (kiali minimum/maximum version is ignored).
-func checkOSSMVersion(ossmVersion string, kialiVersion string) bool {
-	ok := false
-	matrix, err := config.NewCompatibilityMatrix()
-
-	if err != nil {
-		return ok
-	}
-
-	// Maistra and Kiali versions should not have "v" prefixes. The compatibility matrix does not have "v" so strip them if they exist.
-	ossmVersion = trimV(ossmVersion)
-	kialiVersion = trimV(kialiVersion)
-
-	// for OSSM, the compatibility matrix only provides X.Y version details - so only check the X.Y portion of the version strings.
-	v := strings.Split(ossmVersion, ".")
-	if len(v) > 1 {
-		ossmVersion = fmt.Sprintf("%v.%v", v[0], v[1])
-	}
-	v = strings.Split(kialiVersion, ".")
-	if len(v) > 1 {
-		kialiVersion = fmt.Sprintf("%v.%v", v[0], v[1])
-	}
-
-	for _, version := range matrix {
-		if version.MeshName == istioProductNameOSSM {
-			for _, versions := range version.VersionRange {
-				if ossmVersion == strings.TrimSpace(versions.MeshVersion) {
-					for _, fixedVersion := range versions.KialiFixedVersion {
-						if kialiVersion == fixedVersion {
-							ok = true
-							break
-						}
-					}
-				}
-			}
-		}
-	}
-
-	return ok
-}
-
-// checkIstioVersion check istio if its version is compatible with kiali
-func checkIstioVersion(istioVersion string, kialiVersion string) bool {
-	ok := false
-	matrix, err := config.NewCompatibilityMatrix()
-
-	if err != nil {
-		return ok
-	}
-
-	for _, version := range matrix {
-		if version.MeshName == istioProductNameUpstream {
-			for _, versions := range version.VersionRange {
-				if strings.Contains(istioVersion, versions.MeshVersion) {
-					minimumVersion := strings.TrimSpace(versions.KialiMinimumVersion)
-					maximumVersion := strings.TrimSpace(versions.KialiMaximumVersion)
-					if fixedVersions := versions.KialiFixedVersion; len(fixedVersions) != 0 {
-						for _, fixedVersion := range fixedVersions {
-							if ok = checkRange(minimumVersion, maximumVersion, fixedVersion, kialiVersion); ok {
-								break
-							}
-						}
-					} else {
-						if ok = checkRange(minimumVersion, maximumVersion, "", kialiVersion); ok {
-							break
-						}
-					}
-
-				}
-			}
-		}
-	}
-	return ok
-}
-
-// checkRange check if version is in target range
-func checkRange(low string, high string, fixed string, version string) bool {
-	ok := true
-	ok1 := true
-	if fixed != "" {
-		equal := "== " + fixed
-		ok = validateVersion(equal, version)
-		return ok
-	}
-
-	if low != "" {
-		low = ">= " + low
-		ok = validateVersion(low, version)
-	}
-
-	if high != "" {
-		high = "<= " + high
-		ok1 = validateVersion(high, version)
-	}
-	return ok && ok1
-}
-
-// CheckVersionCompatibility check mesh name/version compatibility with kiali
-// Only log warnings one time when starting kiali up
-func CheckVersionCompatibility() {
-	istioInfo, err := istioVersion()
-	if err != nil {
-		log.Warning(err)
-	} else if istioInfo.Name == istioProductNameUnknown {
-		log.Warning("Unknown Istio implementation version " + istioInfo.Version + " is not recognized, thus not supported.")
-	} else {
-		log.Infof("Mesh Name: [%v], Mesh Version: [%v]", istioInfo.Name, istioInfo.Version)
-	}
-}
-
 // istioVersion returns the current istio version information
-// add warnings when mesh version is incompatible with kiali
 func istioVersion() (*ExternalServiceInfo, error) {
 	istioConfig := config.Get().ExternalServices.Istio
 	body, code, _, err := httputil.HttpGet(istioConfig.UrlServiceVersion, nil, 10*time.Second, nil, nil)
@@ -255,20 +103,9 @@ func istioVersion() (*ExternalServiceInfo, error) {
 
 	istioInfo := parseIstioRawVersion(rawVersion)
 	meshName, meshVersion := istioInfo.Name, istioInfo.Version
-	kialiVersion, _ := GetStatus(CoreVersion)
-
-	compatibleWarnings := fmt.Sprintf("Kiali [%v] may not be compatible with [%v %v], and is not recommended. See kiali.io for version compatibility",
-		kialiVersion, meshName, meshVersion)
 
-	if ok := CheckMeshVersion(meshName, meshVersion, kialiVersion); ok {
-		Put(MeshVersion, meshVersion)
-		Put(MeshName, meshName)
-	} else {
-		Put(MeshVersion, meshVersion)
-		Put(MeshName, meshName)
-		AddWarningMessages(compatibleWarnings)
-		return nil, fmt.Errorf(compatibleWarnings)
-	}
+	Put(MeshVersion, meshVersion)
+	Put(MeshName, meshName)
 
 	return istioInfo, nil
 }
@@ -472,15 +309,3 @@ func kubernetesVersion() (*ExternalServiceInfo, error) {
 func isMaistraExternalService(esi *ExternalServiceInfo) bool {
 	return esi.Name == istioProductNameOSSM || esi.Name == istioProductNameMaistra || esi.Name == istioProductNameMaistraProject
 }
-
-// trimV will trim the (optional) "v" character found at the beginning of the given version string.
-func trimV(v string) string {
-	if len(v) > 0 {
-		if v[0] == 'v' {
-			return v[1:]
-		} else {
-			return v
-		}
-	}
-	return v
-}
diff --git a/util/httputil/port_forwarder.go b/util/httputil/port_forwarder.go
index 06e9abf43..c122f96a0 100644
--- a/util/httputil/port_forwarder.go
+++ b/util/httputil/port_forwarder.go
@@ -50,11 +50,11 @@ func (f forwarder) Stop() {
 	Pool.FreePort(f.localPort)
 }
 
-func NewPortForwarder(client *rest.Interface, clientConfig *rest.Config, namespace, pod, address, portMap string, writer io.Writer) (PortForwarder, error) {
+func NewPortForwarder(client rest.Interface, clientConfig *rest.Config, namespace, pod, address, portMap string, writer io.Writer) (PortForwarder, error) {
 	stopCh := make(chan struct{})
 	readyCh := make(chan struct{})
 
-	forwarderUrl := (*client).Post().
+	forwarderUrl := client.Post().
 		Namespace(namespace).
 		Resource("pods").
 		Name(pod).



Test patch:
diff --git a/business/apps_test.go b/business/apps_test.go
index f36aea649..cd56c9729 100644
--- a/business/apps_test.go
+++ b/business/apps_test.go
@@ -8,6 +8,7 @@ import (
 	osproject_v1 "github.com/openshift/api/project/v1"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
 	apps_v1 "k8s.io/api/apps/v1"
 	batch_v1 "k8s.io/api/batch/v1"
 	core_v1 "k8s.io/api/core/v1"
@@ -27,7 +28,10 @@ func setupAppService(k8s *kubetest.K8SClientMock) *AppService {
 
 func TestGetAppListFromDeployments(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
+
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 
 	// Setup mocks
@@ -47,10 +51,12 @@ func TestGetAppListFromDeployments(t *testing.T) {
 	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
 	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.Pod{}, nil)
 	k8s.On("GetServices", mock.AnythingOfType("string"), mock.AnythingOfType("map[string]string")).Return([]core_v1.Service{}, nil)
+	k8s.On("GetToken").Return("token")
 	svc := setupAppService(k8s)
 
 	criteria := AppCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
-	appList, _ := svc.GetAppList(context.TODO(), criteria)
+	appList, err := svc.GetAppList(context.TODO(), criteria)
+	require.NoError(err)
 
 	assert.Equal("Namespace", appList.Namespace.Name)
 
@@ -78,6 +84,7 @@ func TestGetAppFromDeployments(t *testing.T) {
 	k8s.On("GetServices", mock.AnythingOfType("string"), mock.AnythingOfType("map[string]string")).Return(FakeServices(), nil)
 
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	conf.ExternalServices.CustomDashboards.Enabled = false
 	config.Set(conf)
 	svc := setupAppService(k8s)
@@ -98,6 +105,7 @@ func TestGetAppFromDeployments(t *testing.T) {
 func TestGetAppListFromReplicaSets(t *testing.T) {
 	assert := assert.New(t)
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 
 	// Setup mocks
@@ -147,6 +155,7 @@ func TestGetAppFromReplicaSets(t *testing.T) {
 	k8s.On("GetServices", mock.AnythingOfType("string"), mock.AnythingOfType("map[string]string")).Return(FakeServices(), nil)
 
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	conf.ExternalServices.CustomDashboards.Enabled = false
 	config.Set(conf)
 
diff --git a/business/authentication/openid_auth_controller_test.go b/business/authentication/openid_auth_controller_test.go
index 587e17adf..cbded517f 100644
--- a/business/authentication/openid_auth_controller_test.go
+++ b/business/authentication/openid_auth_controller_test.go
@@ -14,7 +14,6 @@ import (
 
 	osproject_v1 "github.com/openshift/api/project/v1"
 	"github.com/stretchr/testify/assert"
-	v1 "k8s.io/api/core/v1"
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/clientcmd/api"
 
@@ -37,7 +36,7 @@ const openIdTestToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQG
 
 /*** Implicit flow tests ***/
 
-func TestOpenIdAuthControllerAuthenticatesCorrectlyWithImplicitFlow(t *testing.T) {
+func TestOpenIdAuthControllerRejectsImplicitFlow(t *testing.T) {
 	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
 	util.Clock = util.ClockMock{Time: clockTime}
 
@@ -73,735 +72,12 @@ func TestOpenIdAuthControllerAuthenticatesCorrectlyWithImplicitFlow(t *testing.T
 	rr := httptest.NewRecorder()
 	sData, err := controller.Authenticate(request, rr)
 
-	expectedExpiration := time.Date(2021, 12, 1, 0, 0, 1, 0, time.UTC)
-
-	assert.Nil(t, err)
-	assert.NotNil(t, sData)
-	assert.Equal(t, "jdoe@domain.com", sData.Username)
-	assert.Equal(t, openIdTestToken, sData.AuthInfo.Token)
-	assert.True(t, expectedExpiration.Equal(sData.ExpiresOn))
-
-	// Simply check that some cookie is set and has the right expiration. Testing cookie content is left to the session_persistor_test.go
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 2)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-
-	// Session cookie
-	assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name)
-	assert.Equal(t, expectedExpiration, response.Cookies()[1].Expires)
-}
-
-func TestOpenIdImplicitFlowShouldRejectMissingToken(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	// Calculate a hash of the wrong string.
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "Token is empty or invalid.", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowShouldRejectTokenWithoutExpiration(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	// same as openIdTestToken but without the "exp" claim
-	oidcToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQifQ.ih34Mh3Sao9bnXCjaobfAEO1BnHnuuLBWxihAzwUqw8"
-
-	// Calculate a hash of the wrong string.
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", oidcToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "the received id_token from the OpenId provider has missing the required 'exp' claim", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowShouldRejectTokenWithNonNumericExpClaim(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	// same as openIdTestToken but with the claim exp=foo
-	oidcToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOiJmb28ifQ.wdM3yQPwAXLaqZbVku_fcXpisC3tzES8_UUwjbxSPrc"
-
-	// Calculate a hash of the wrong string.
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", oidcToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "token exp claim is present, but invalid: the 'exp' claim of the OpenId token has invalid type", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowShouldRejectRequestWithInvalidState(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	// Calculate a hash of the wrong string.
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "badNonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "Request rejected: CSRF mitigation", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowShouldRejectRequestWithBadStateFormat(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	// Calculate a hash of the wrong string.
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x+%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "Request rejected: State parameter is invalid", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowShouldRejectRequestWithMissingState(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s", openIdTestToken))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "State parameter is empty or invalid.", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowShouldRejectRequestWithMissingNonceCookie(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "No nonce code present. Login window timed out.", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowShouldRejectRequestWithMissingNonceInToken(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	// same as openIdTestToken, but without the nonce claim.
-	oidcToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTMxMTI4MTk3MH0.xAoq7T-wti__Je1PDuTgNonoVSu059FzpOHsNm26YTg"
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", oidcToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		return nil, errors.New("business instantiator shouldn't have been called")
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Nil(t, sData)
-	assert.Equal(t, "OpenId token rejected: nonce code mismatch", err.Error())
-
-	// Check cookies
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowRejectsTokenWithoutDomainIfAllowedDomainsAreConfigured(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	cfg.Auth.OpenId.AllowedDomains = []string{
-		"foo.com",
-	}
-	config.Set(cfg)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		assert.Failf(t, "business instantiator shouldn't have been called", "")
-		return nil, nil
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Equal(t, "cannot detect hosted domain on OpenID for the email  ", err.Error())
-	assert.Nil(t, sData)
-
-	// nonce cookie cleanup
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowRejectsTokenWithoutAnAllowedDomainInEmailClaim(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	cfg.Auth.OpenId.AllowedDomains = []string{
-		"foo.com",
-	}
-	config.Set(cfg)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	// Same as openIdTestToken, but with an added email=jdoe@domain.com claim
-	oidcToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJlbWFpbCI6Impkb2VAZG9tYWluLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwibm9uY2UiOiIxYmE5YjgzNGQwOGFjODFmZWIzNGUyMDg0MDJlYjE4ZTkwOWJlMDg0NTE4YzMyODUxMDk0MDE4NCIsImV4cCI6MTYzODMxNjgwMX0.8oA-SgrQveJgmzCVOCrAQyQlswYwlWMAuUvGMJ8T748"
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", oidcToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		assert.Failf(t, "business instantiator shouldn't have been called", "")
-		return nil, nil
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Equal(t, "domain domain.com not allowed to login", err.Error())
-	assert.Nil(t, sData)
-
-	// nonce cookie cleanup
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowRejectsTokenWithoutAnAllowedDomainInHdClaim(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	cfg.Auth.OpenId.AllowedDomains = []string{
-		"foo.com",
-	}
-	config.Set(cfg)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	// Same as openIdTestToken, but with added email=jdoe@foo.com and hd=domail.com claim
-	oidcToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJlbWFpbCI6Impkb2VAZm9vLmNvbSIsImhkIjoiZG9tYWluLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwibm9uY2UiOiIxYmE5YjgzNGQwOGFjODFmZWIzNGUyMDg0MDJlYjE4ZTkwOWJlMDg0NTE4YzMyODUxMDk0MDE4NCIsImV4cCI6MTYzODMxNjgwMX0.LambAMoRezERKTUfZBgCb5h-DEVEW2enQOVxnieG8K4"
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", oidcToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		assert.Failf(t, "business instantiator shouldn't have been called", "")
-		return nil, nil
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.Equal(t, "domain domain.com not allowed to login", err.Error())
-	assert.Nil(t, sData)
-
-	// nonce cookie cleanup
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowAllowsLoginWithAllowedDomainInHdClaim(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	cfg.Auth.OpenId.AllowedDomains = []string{
-		"domain.com",
-	}
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	// Same as openIdTestToken, but with added email=jdoe@foo.com and hd=domail.com claim
-	oidcToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJlbWFpbCI6Impkb2VAZm9vLmNvbSIsImhkIjoiZG9tYWluLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwibm9uY2UiOiIxYmE5YjgzNGQwOGFjODFmZWIzNGUyMDg0MDJlYjE4ZTkwOWJlMDg0NTE4YzMyODUxMDk0MDE4NCIsImV4cCI6MTYzODMxNjgwMX0.LambAMoRezERKTUfZBgCb5h-DEVEW2enQOVxnieG8K4"
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", oidcToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		if authInfo.Token != oidcToken {
-			return nil, errors.New("unexpected token")
-		}
-		return business.NewWithBackends(k8s, nil, nil), nil
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	expectedExpiration := time.Date(2021, 12, 1, 0, 0, 1, 0, time.UTC)
-
-	assert.Nil(t, err)
-	assert.NotNil(t, sData)
-	assert.Equal(t, "jdoe@domain.com", sData.Username)
-	assert.Equal(t, oidcToken, sData.AuthInfo.Token)
-	assert.True(t, expectedExpiration.Equal(sData.ExpiresOn))
-
-	// Simply check that some cookie is set and has the right expiration. Testing cookie content is left to the session_persistor_test.go
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 2)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-
-	// Session cookie
-	assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name)
-	assert.Equal(t, expectedExpiration, response.Cookies()[1].Expires)
-}
-
-func TestOpenIdImplicitFlowAllowsLoginWithAllowedDomainInEmailClaim(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	cfg.Auth.OpenId.AllowedDomains = []string{
-		"domain.com",
-	}
-	config.Set(cfg)
-
-	// Returning some namespace when a cluster API call is made should have the result of
-	// a successful authentication.
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{
-		{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
-	}, nil)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	// Same as openIdTestToken, but with an added email=jdoe@domain.com claim
-	oidcToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJlbWFpbCI6Impkb2VAZG9tYWluLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwibm9uY2UiOiIxYmE5YjgzNGQwOGFjODFmZWIzNGUyMDg0MDJlYjE4ZTkwOWJlMDg0NTE4YzMyODUxMDk0MDE4NCIsImV4cCI6MTYzODMxNjgwMX0.8oA-SgrQveJgmzCVOCrAQyQlswYwlWMAuUvGMJ8T748"
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", oidcToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		if authInfo.Token != oidcToken {
-			return nil, errors.New("unexpected token")
-		}
-		return business.NewWithBackends(k8s, nil, nil), nil
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	expectedExpiration := time.Date(2021, 12, 1, 0, 0, 1, 0, time.UTC)
-
-	assert.Nil(t, err)
-	assert.NotNil(t, sData)
-	assert.Equal(t, "jdoe@domain.com", sData.Username)
-	assert.Equal(t, oidcToken, sData.AuthInfo.Token)
-	assert.True(t, expectedExpiration.Equal(sData.ExpiresOn))
-
-	// Simply check that some cookie is set and has the right expiration. Testing cookie content is left to the session_persistor_test.go
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 2)
-
-	// nonce cookie cleanup
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-
-	// Session cookie
-	assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name)
-	assert.Equal(t, expectedExpiration, response.Cookies()[1].Expires)
-}
-
-func TestOpenIdImplicitFlowRejectsTokenWithoutPrivileges(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// No namespaces should result in auth failure
-	k8s := kubetest.NewK8SClientMock()
-	k8s.On("GetProjects", "").Return([]osproject_v1.Project{}, nil)
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		if authInfo.Token != openIdTestToken {
-			return nil, errors.New("unexpected token")
-		}
-		return business.NewWithBackends(k8s, nil, nil), nil
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
-	assert.NotNil(t, err)
-	assert.IsType(t, &AuthenticationFailureError{}, err)
-	assert.Equal(t, 401, err.(*AuthenticationFailureError).HttpStatus)
-	assert.Contains(t, err.Error(), "RBAC")
 	assert.Nil(t, sData)
-
-	// nonce cookie cleanup
-	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
-}
-
-func TestOpenIdImplicitFlowRejectsTokenNotAcceptedByK8sAPI(t *testing.T) {
-	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
-	util.Clock = util.ClockMock{Time: clockTime}
-
-	cfg := config.NewConfig()
-	cfg.LoginToken.SigningKey = "kiali67890123456"
-	cfg.LoginToken.ExpirationSeconds = 1
-	config.Set(cfg)
-
-	// Error from API to simulate a bad token
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetKialiToken").Return("")
-	k8s.On("GetNamespaces", "").Return([]v1.Namespace{}, errors.New("token rejected"))
-
-	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), config.GetSigningKey())))
-	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405")))
-	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
-	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
-	request.AddCookie(&http.Cookie{
-		Name:  OpenIdNonceCookieName,
-		Value: "nonceString",
-	})
-
-	controller := NewOpenIdAuthController(CookieSessionPersistor{}, func(authInfo *api.AuthInfo) (*business.Layer, error) {
-		if authInfo.Token != openIdTestToken {
-			return nil, errors.New("unexpected token")
-		}
-		return business.NewWithBackends(k8s, nil, nil), nil
-	})
-
-	rr := httptest.NewRecorder()
-	sData, err := controller.Authenticate(request, rr)
-
 	assert.NotNil(t, err)
-	assert.IsType(t, &AuthenticationFailureError{}, err)
-	assert.Equal(t, 401, err.(*AuthenticationFailureError).HttpStatus)
-	assert.Contains(t, err.Error(), "Token is not valid")
-	assert.Nil(t, sData)
+	assert.Equal(t, err.Error(), "support for OpenID's implicit flow has been removed")
 
-	// nonce cookie cleanup
 	response := rr.Result()
-	assert.Len(t, response.Cookies(), 1)
-	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
-	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
+	assert.Len(t, response.Cookies(), 0)
 }
 
 /*** Authorization code flow tests ***/
diff --git a/business/checkers/authorization/principals_checker_test.go b/business/checkers/authorization/principals_checker_test.go
index 76eef621a..4b6224c99 100644
--- a/business/checkers/authorization/principals_checker_test.go
+++ b/business/checkers/authorization/principals_checker_test.go
@@ -24,6 +24,38 @@ func TestPresentServiceAccount(t *testing.T) {
 	assert.Empty(validations)
 }
 
+func TestRegexPrincipalFound(t *testing.T) {
+	assert := assert.New(t)
+
+	validations, valid := PrincipalsChecker{
+		AuthorizationPolicy: authPolicyWithPrincipals([]string{"*local/ns/bookinfo/sa/default*", "*.local/ns/bookinfo/sa/test*"}),
+		ServiceAccounts:     []string{"cluster.local/ns/bookinfo/sa/default-a", "cluster.local/ns/bookinfo/sa/test-1"},
+	}.Check()
+
+	// regex matches
+	assert.True(valid)
+	assert.Empty(validations)
+}
+
+func TestRegexPrincipalNotFound(t *testing.T) {
+	assert := assert.New(t)
+
+	vals, valid := PrincipalsChecker{
+		AuthorizationPolicy: authPolicyWithPrincipals([]string{"*wronglocal/ns/bookinfo/sa/default*", "*.local/ns/bookinfo/sa/test1*"}),
+		ServiceAccounts:     []string{"cluster.local/ns/bookinfo/sa/default-a", "cluster.local/ns/bookinfo/sa/test-1"},
+	}.Check()
+
+	assert.False(valid)
+	assert.NotEmpty(vals)
+	assert.Len(vals, 2)
+	assert.Equal(models.ErrorSeverity, vals[0].Severity)
+	assert.Error(validations.ConfirmIstioCheckMessage("authorizationpolicy.nodest.principalnotfound", vals[0]))
+	assert.Equal("spec/rules[0]/from[0]/source/principals[0]", vals[0].Path)
+	assert.Equal(models.ErrorSeverity, vals[1].Severity)
+	assert.Error(validations.ConfirmIstioCheckMessage("authorizationpolicy.nodest.principalnotfound", vals[1]))
+	assert.Equal("spec/rules[0]/from[0]/source/principals[1]", vals[1].Path)
+}
+
 func TestEmptyPrincipals(t *testing.T) {
 	assert := assert.New(t)
 
diff --git a/business/checkers/k8sgateways/multi_match_checker_test.go b/business/checkers/k8sgateways/multi_match_checker_test.go
new file mode 100644
index 000000000..036399bf3
--- /dev/null
+++ b/business/checkers/k8sgateways/multi_match_checker_test.go
@@ -0,0 +1,181 @@
+package k8sgateways
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/tests/data"
+)
+
+func TestCorrectK8sGateways(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	k8sgwObject := data.CreateEmptyK8sGateway("validk8sgateway", "test")
+
+	k8sgws := []*k8s_networking_v1beta1.Gateway{k8sgwObject}
+
+	vals := MultiMatchChecker{
+		K8sGateways: k8sgws,
+	}.Check()
+
+	assert.Empty(vals)
+	_, ok := vals[models.IstioValidationKey{ObjectType: "k8sgateway", Namespace: "test", Name: "validk8sgateway"}]
+	assert.False(ok)
+}
+
+func TestDuplicateListenersCheckError(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	k8sgwObject := data.AddListenerToK8sGateway(data.CreateListener("test", "host.com", 80, "http"),
+		data.CreateEmptyK8sGateway("validk8sgateway", "test"))
+
+	k8sgwObject2 := data.AddListenerToK8sGateway(data.CreateListener("test", "host.com", 80, "http"),
+		data.CreateEmptyK8sGateway("validk8sgateway2", "test"))
+
+	k8sgws := []*k8s_networking_v1beta1.Gateway{k8sgwObject, k8sgwObject2}
+
+	vals := MultiMatchChecker{
+		K8sGateways: k8sgws,
+	}.Check()
+
+	assert.NotEmpty(vals)
+	assert.Equal(2, len(vals))
+	validation, ok := vals[models.IstioValidationKey{ObjectType: "k8sgateway", Namespace: "test", Name: "validk8sgateway2"}]
+	assert.True(ok)
+	assert.NotNil(validation)
+	assert.True(validation.Valid)
+	assert.Greater(len(validation.Checks), 0)
+}
+
+func TestDuplicateListenersCheckOk(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	k8sgwObject := data.AddListenerToK8sGateway(data.CreateListener("test", "host.es", 80, "http"),
+		data.CreateEmptyK8sGateway("validk8sgateway", "test"))
+
+	k8sgwObject2 := data.AddListenerToK8sGateway(data.CreateListener("test", "host.com", 80, "http"),
+		data.CreateEmptyK8sGateway("validk8sgateway2", "test"))
+
+	k8sgws := []*k8s_networking_v1beta1.Gateway{k8sgwObject, k8sgwObject2}
+
+	vals := MultiMatchChecker{
+		K8sGateways: k8sgws,
+	}.Check()
+
+	assert.Empty(vals)
+
+}
+
+func TestDuplicateAddresssCheckError(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	gwAddress := data.CreateGWAddress("IPAddress", "192.168.0.0")
+	k8sgwObject := data.AddGwAddressToK8sGateway(gwAddress,
+		data.CreateEmptyK8sGateway("validk8sgateway", "test"))
+
+	k8sgwObject2 := data.AddGwAddressToK8sGateway(gwAddress,
+		data.CreateEmptyK8sGateway("validk8sgateway2", "test"))
+
+	k8sgws := []*k8s_networking_v1beta1.Gateway{k8sgwObject, k8sgwObject2}
+
+	vals := MultiMatchChecker{
+		K8sGateways: k8sgws,
+	}.Check()
+
+	assert.NotEmpty(vals)
+	assert.Equal(2, len(vals))
+	validation, ok := vals[models.IstioValidationKey{ObjectType: "k8sgateway", Namespace: "test", Name: "validk8sgateway2"}]
+	assert.True(ok)
+	assert.NotNil(validation)
+	assert.True(validation.Valid)
+	assert.Greater(len(validation.Checks), 0)
+
+}
+
+func TestDuplicateAddresssCheckOk(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	gwAddress := data.CreateGWAddress("IPAddress", "192.168.0.0")
+	k8sgwObject := data.AddGwAddressToK8sGateway(gwAddress,
+		data.CreateEmptyK8sGateway("validk8sgateway", "test"))
+
+	gwAddress2 := data.CreateGWAddress("IPAddress", "127.0.0.1")
+	k8sgwObject2 := data.AddGwAddressToK8sGateway(gwAddress2,
+		data.CreateEmptyK8sGateway("validk8sgateway2", "test"))
+
+	k8sgws := []*k8s_networking_v1beta1.Gateway{k8sgwObject, k8sgwObject2}
+
+	vals := MultiMatchChecker{
+		K8sGateways: k8sgws,
+	}.Check()
+
+	assert.Empty(vals)
+
+}
+
+func TestUniqueListenerOk(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	k8sgwObject := data.AddListenerToK8sGateway(data.CreateListener("test", "host.es", 80, "http"),
+		data.CreateEmptyK8sGateway("validk8sgateway", "test"))
+
+	k8sgwObject = data.AddListenerToK8sGateway(data.CreateListener("test2", "host.com", 80, "http"),
+		k8sgwObject)
+
+	k8sgws := []*k8s_networking_v1beta1.Gateway{k8sgwObject}
+
+	vals := MultiMatchChecker{
+		K8sGateways: k8sgws,
+	}.Check()
+
+	assert.Empty(vals)
+
+}
+
+func TestUniqueListenerDuplicate(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	k8sgwObject := data.AddListenerToK8sGateway(data.CreateListener("test", "host.es", 80, "http"),
+		data.CreateEmptyK8sGateway("k8sgateway", "test"))
+
+	k8sgwObject = data.AddListenerToK8sGateway(data.CreateListener("test2", "host.es", 80, "http"),
+		k8sgwObject)
+
+	k8sgws := []*k8s_networking_v1beta1.Gateway{k8sgwObject}
+
+	vals := MultiMatchChecker{
+		K8sGateways: k8sgws,
+	}.Check()
+
+	assert.NotEmpty(vals)
+	assert.Equal(1, len(vals))
+	validation, ok := vals[models.IstioValidationKey{ObjectType: "k8sgateway", Namespace: "test", Name: "k8sgateway"}]
+	assert.True(ok)
+	assert.Greater(len(validation.Checks), 0)
+
+}
diff --git a/business/checkers/k8sgateways/status_checker_test.go b/business/checkers/k8sgateways/status_checker_test.go
new file mode 100644
index 000000000..254e99f0e
--- /dev/null
+++ b/business/checkers/k8sgateways/status_checker_test.go
@@ -0,0 +1,49 @@
+package k8sgateways
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/tests/data"
+)
+
+func TestCorrectK8sGatewaysStatus(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	k8sgwObject := data.CreateEmptyK8sGateway("validgateway", "test")
+
+	k8sgws := StatusChecker{k8sgwObject}
+
+	check, isValid := k8sgws.Check()
+
+	assert.True(isValid)
+	assert.Empty(check)
+}
+
+func TestIncorrectK8sGatewaysStatus(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	assert := assert.New(t)
+
+	gwAddress := data.CreateGWAddress("IPAddress", "192.168.0.0")
+	k8sgwObject := data.AddListenerToK8sGateway(data.CreateListener("test", "host.com.wrong", 11, "http"),
+		data.CreateEmptyK8sGateway("validk8sgateway", "test"))
+	k8sgwObject = data.AddGwAddressToK8sGateway(gwAddress, k8sgwObject)
+	k8sgwObject = data.UpdateConditionWithError(k8sgwObject)
+
+	k8sgws := StatusChecker{k8sgwObject}
+
+	check, isValid := k8sgws.Check()
+
+	assert.False(isValid)
+	assert.NotEmpty(check)
+	assert.Equal("Fake msg. GWAPI errors should be changed in the spec.", check[0].Message)
+	assert.Equal(models.WarningSeverity, check[0].Severity)
+}
diff --git a/business/checkers/k8shttproute_checker_test.go b/business/checkers/k8shttproute_checker_test.go
new file mode 100644
index 000000000..920abc410
--- /dev/null
+++ b/business/checkers/k8shttproute_checker_test.go
@@ -0,0 +1,75 @@
+package checkers
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/tests/data"
+	"github.com/kiali/kiali/tests/testutils/validations"
+)
+
+func TestNoCrashOnEmptyRoute(t *testing.T) {
+	assert := assert.New(t)
+
+	typeValidations := K8sHTTPRouteChecker{
+		K8sHTTPRoutes:    []*k8s_networking_v1beta1.HTTPRoute{},
+		K8sGateways:      []*k8s_networking_v1beta1.Gateway{},
+		RegistryServices: data.CreateEmptyRegistryServices(),
+		Namespaces:       models.Namespaces{},
+	}.Check()
+
+	assert.Empty(typeValidations)
+}
+
+func TestWithoutK8sGateway(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+	assert := assert.New(t)
+
+	vals := K8sHTTPRouteChecker{
+		K8sHTTPRoutes: []*k8s_networking_v1beta1.HTTPRoute{
+			data.CreateHTTPRoute("route1", "bookinfo", "gatewayapi", []string{"bookinfo"}),
+			data.CreateHTTPRoute("route2", "bookinfo", "gatewayapi2", []string{"bookinfo"})},
+		K8sGateways: []*k8s_networking_v1beta1.Gateway{data.CreateEmptyK8sGateway("gatewayapiwrong", "bookinfo")},
+	}.Check()
+
+	assert.NotEmpty(vals)
+
+	route1 := vals[models.IstioValidationKey{ObjectType: "k8shttproute", Namespace: "bookinfo", Name: "route1"}]
+	assert.False(route1.Valid)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nok8sgateway", route1.Checks[0]))
+	route2 := vals[models.IstioValidationKey{ObjectType: "k8shttproute", Namespace: "bookinfo", Name: "route2"}]
+	assert.False(route2.Valid)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nok8sgateway", route2.Checks[0]))
+}
+
+func TestWithoutService(t *testing.T) {
+	conf := config.NewConfig()
+	config.Set(conf)
+	assert := assert.New(t)
+
+	registryService1 := data.CreateFakeRegistryServices("other.bookinfo.svc.cluster.local", "bookinfo", "*")
+	registryService2 := data.CreateFakeRegistryServices("details.bookinfo.svc.cluster.local", "bookinfo2", "*")
+
+	vals := K8sHTTPRouteChecker{
+		K8sHTTPRoutes: []*k8s_networking_v1beta1.HTTPRoute{
+			data.AddBackendRefToHTTPRoute("ratings", "bookinfo", data.CreateHTTPRoute("route1", "bookinfo", "gatewayapi", []string{"bookinfo"})),
+			data.AddBackendRefToHTTPRoute("ratings", "bookinfo", data.CreateHTTPRoute("route2", "bookinfo2", "gatewayapi2", []string{"bookinfo2"}))},
+		K8sGateways:      []*k8s_networking_v1beta1.Gateway{data.CreateEmptyK8sGateway("gatewayapi", "bookinfo"), data.CreateEmptyK8sGateway("gatewayapi2", "bookinfo2")},
+		RegistryServices: append(registryService1, registryService2...),
+		Namespaces:       models.Namespaces{models.Namespace{Name: "bookinfo"}, models.Namespace{Name: "bookinfo2"}, models.Namespace{Name: "bookinfo3"}},
+	}.Check()
+
+	assert.NotEmpty(vals)
+
+	route1 := vals[models.IstioValidationKey{ObjectType: "k8shttproute", Namespace: "bookinfo", Name: "route1"}]
+	assert.False(route1.Valid)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nohost.namenotfound", route1.Checks[0]))
+	route2 := vals[models.IstioValidationKey{ObjectType: "k8shttproute", Namespace: "bookinfo2", Name: "route2"}]
+	assert.False(route2.Valid)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nohost.namenotfound", route2.Checks[0]))
+}
diff --git a/business/checkers/k8shttproutes/no_host_checker_test.go b/business/checkers/k8shttproutes/no_host_checker_test.go
new file mode 100644
index 000000000..2161926d8
--- /dev/null
+++ b/business/checkers/k8shttproutes/no_host_checker_test.go
@@ -0,0 +1,100 @@
+package k8shttproutes
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/tests/data"
+	"github.com/kiali/kiali/tests/testutils/validations"
+)
+
+func TestValidRefHost(t *testing.T) {
+	c := config.Get()
+	c.ExternalServices.Istio.IstioIdentityDomain = "svc.cluster.local"
+	config.Set(c)
+
+	assert := assert.New(t)
+
+	registryService1 := data.CreateFakeRegistryServices("other.bookinfo.svc.cluster.local", "bookinfo", "*")
+	registryService2 := data.CreateFakeRegistryServices("reviews.bookinfo.svc.cluster.local", "bookinfo", "*")
+
+	vals, valid := NoHostChecker{
+		RegistryServices: append(registryService1, registryService2...),
+		K8sHTTPRoute:     data.AddBackendRefToHTTPRoute("reviews", "bookinfo", data.CreateHTTPRoute("route", "bookinfo2", "gatewayapi", []string{"bookinfo"})),
+	}.Check()
+
+	assert.True(valid)
+	assert.Empty(vals)
+}
+
+func TestValidRefHostDefaultNs(t *testing.T) {
+	c := config.Get()
+	c.ExternalServices.Istio.IstioIdentityDomain = "svc.cluster.local"
+	config.Set(c)
+
+	assert := assert.New(t)
+
+	registryService1 := data.CreateFakeRegistryServices("other.bookinfo.svc.cluster.local", "bookinfo", "*")
+	registryService2 := data.CreateFakeRegistryServices("reviews.bookinfo.svc.cluster.local", "bookinfo", "*")
+
+	vals, valid := NoHostChecker{
+		RegistryServices: append(registryService1, registryService2...),
+		K8sHTTPRoute:     data.AddBackendRefToHTTPRoute("reviews", "", data.CreateHTTPRoute("route", "bookinfo", "gatewayapi", []string{"bookinfo"})),
+	}.Check()
+
+	assert.True(valid)
+	assert.Empty(vals)
+}
+
+func TestInvalidRefHostDefaultNs(t *testing.T) {
+	c := config.Get()
+	c.ExternalServices.Istio.IstioIdentityDomain = "svc.cluster.local"
+	config.Set(c)
+
+	assert := assert.New(t)
+
+	registryService1 := data.CreateFakeRegistryServices("other.bookinfo.svc.cluster.local", "bookinfo", "*")
+	registryService2 := data.CreateFakeRegistryServices("reviews.bookinfo.svc.cluster.local", "bookinfo", "*")
+
+	vals, valid := NoHostChecker{
+		RegistryServices: append(registryService1, registryService2...),
+		K8sHTTPRoute:     data.AddBackendRefToHTTPRoute("reviews", "", data.CreateHTTPRoute("route", "bookinfo2", "gatewayapi", []string{"bookinfo"})),
+	}.Check()
+
+	assert.False(valid)
+	assert.NotEmpty(vals)
+	assert.Len(vals, 1)
+	assert.Equal(models.ErrorSeverity, vals[0].Severity)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nohost.namenotfound", vals[0]))
+	assert.Equal("spec/rules[0]/backendRefs[0]/name", vals[0].Path)
+}
+
+func TestNoValidRefHost(t *testing.T) {
+	c := config.Get()
+	c.ExternalServices.Istio.IstioIdentityDomain = "svc.cluster.local"
+	config.Set(c)
+
+	registryService1 := data.CreateFakeRegistryServices("other.bookinfo.svc.cluster.local", "bookinfo", "*")
+	registryService2 := data.CreateFakeRegistryServices("details.bookinfo.svc.cluster.local", "bookinfo", "*")
+
+	assert := assert.New(t)
+
+	vals, valid := NoHostChecker{
+		RegistryServices: append(registryService1, registryService2...),
+		K8sHTTPRoute:     data.AddBackendRefToHTTPRoute("ratings", "bookinfo", data.AddBackendRefToHTTPRoute("reviews", "bookinfo", data.CreateHTTPRoute("route", "bookinfo2", "gatewayapi", []string{"bookinfo2"}))),
+	}.Check()
+
+	assert.False(valid)
+	assert.NotEmpty(vals)
+	assert.Len(vals, 2)
+	assert.Equal(models.ErrorSeverity, vals[0].Severity)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nohost.namenotfound", vals[0]))
+	assert.Equal("spec/rules[0]/backendRefs[0]/name", vals[0].Path)
+
+	assert.Equal(models.ErrorSeverity, vals[1].Severity)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nohost.namenotfound", vals[1]))
+	assert.Equal("spec/rules[1]/backendRefs[0]/name", vals[1].Path)
+}
diff --git a/business/checkers/k8shttproutes/no_k8sgateway_checker_test.go b/business/checkers/k8shttproutes/no_k8sgateway_checker_test.go
new file mode 100644
index 000000000..7468c09cf
--- /dev/null
+++ b/business/checkers/k8shttproutes/no_k8sgateway_checker_test.go
@@ -0,0 +1,89 @@
+package k8shttproutes
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/tests/data"
+	"github.com/kiali/kiali/tests/testutils/validations"
+)
+
+func TestMissingK8sGateway(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	checker := NoK8sGatewayChecker{
+		K8sHTTPRoute: data.CreateHTTPRoute("route", "bookinfo", "gatewayapi", []string{"bookinfo"}),
+		GatewayNames: make(map[string]struct{}),
+	}
+
+	vals, valid := checker.Check()
+	assert.False(valid)
+	assert.NotEmpty(vals)
+	assert.Equal(models.ErrorSeverity, vals[0].Severity)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nok8sgateway", vals[0]))
+}
+
+func TestMissingK8sGateways(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	checker := NoK8sGatewayChecker{
+		K8sHTTPRoute: data.AddParentRefToHTTPRoute("gateway2", "bookinfo2",
+			data.CreateHTTPRoute("route", "bookinfo", "gatewayapi", []string{"bookinfo"})),
+		GatewayNames: make(map[string]struct{}),
+	}
+
+	vals, valid := checker.Check()
+	assert.False(valid)
+	assert.NotEmpty(vals)
+	assert.Len(vals, 2)
+	assert.Equal(models.ErrorSeverity, vals[0].Severity)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nok8sgateway", vals[0]))
+	assert.Equal(models.ErrorSeverity, vals[1].Severity)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nok8sgateway", vals[1]))
+}
+
+func TestValidAndMissingK8sGateway(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	var empty struct{}
+
+	checker := NoK8sGatewayChecker{
+		K8sHTTPRoute: data.AddParentRefToHTTPRoute("correctgw", "bookinfo2",
+			data.CreateHTTPRoute("route", "bookinfo", "gatewayapi", []string{"bookinfo"})),
+		GatewayNames: map[string]struct{}{"correctgw": empty},
+	}
+
+	vals, valid := checker.Check()
+	assert.False(valid)
+	assert.NotEmpty(vals)
+	assert.Equal(models.ErrorSeverity, vals[0].Severity)
+	assert.NoError(validations.ConfirmIstioCheckMessage("k8shttproutes.nok8sgateway", vals[0]))
+}
+
+func TestFoundK8sGateway(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	checker := NoK8sGatewayChecker{
+		K8sHTTPRoute: data.CreateHTTPRoute("route", "bookinfo", "my-gateway", []string{"bookinfo"}),
+		GatewayNames: kubernetes.K8sGatewayNames([]*k8s_networking_v1beta1.Gateway{
+			data.CreateEmptyK8sGateway("my-gateway", "bookinfo"),
+		}),
+	}
+
+	vals, valid := checker.Check()
+	assert.True(valid)
+	assert.Empty(vals)
+}
diff --git a/business/health_test.go b/business/health_test.go
index 8c4d9e58c..4f9309b6d 100644
--- a/business/health_test.go
+++ b/business/health_test.go
@@ -12,9 +12,9 @@ import (
 	apps_v1 "k8s.io/api/apps/v1"
 	core_v1 "k8s.io/api/core/v1"
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/kiali/kiali/config"
-	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/kubernetes/kubetest"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/prometheus/prometheustest"
@@ -25,21 +25,19 @@ var emptyResult = map[string]map[string]float64{}
 func TestGetServiceHealth(t *testing.T) {
 	assert := assert.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
+	k8s := kubetest.NewFakeK8sClient(
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+		&core_v1.Service{ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "ns"}},
+	)
+	k8s.OpenShift = true
+
 	prom := new(prometheustest.PromClientMock)
-	conf := config.NewConfig()
-	config.Set(conf)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
 	prom.MockServiceRequestRates("ns", "httpbin", serviceRates)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetService", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&core_v1.Service{}, nil)
 
 	setupGlobalMeshConfig()
-	hs := HealthService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	hs := HealthService{prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
 
 	mockSvc := models.Service{}
 	mockSvc.Name = "httpbin"
@@ -64,21 +62,23 @@ func TestGetServiceHealth(t *testing.T) {
 func TestGetAppHealth(t *testing.T) {
 	assert := assert.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
+	objects := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+		&core_v1.Service{ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "ns"}},
+	}
+	for _, obj := range fakeDeploymentsHealthReview() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	for _, obj := range fakePodsHealthReview() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	k8s.OpenShift = true
 	prom := new(prometheustest.PromClientMock)
-	conf := config.NewConfig()
-	config.Set(conf)
 
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.MockEmptyWorkloads("ns")
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", "ns").Return(fakeDeploymentsHealthReview(), nil)
-	k8s.On("GetPods", "ns", "app=reviews").Return(fakePodsHealthReview(), nil)
-	k8s.On("GetProxyStatus").Return([]*kubernetes.ProxyStatus{}, nil)
-
-	hs := HealthService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	hs := HealthService{prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
 	prom.MockAppRequestRates("ns", "reviews", otherRatesIn, otherRatesOut)
@@ -116,24 +116,23 @@ func TestGetAppHealth(t *testing.T) {
 func TestGetWorkloadHealth(t *testing.T) {
 	assert := assert.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
+	objects := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+		&core_v1.Service{ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "ns"}},
+		&fakeDeploymentsHealthReview()[0],
+	}
+	for _, obj := range fakePodsHealthReview() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	k8s.OpenShift = true
 	prom := new(prometheustest.PromClientMock)
-	conf := config.NewConfig()
-	config.Set(conf)
-
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.MockEmptyWorkload("ns", "reviews-v1")
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployment", "ns", "reviews-v1").Return(&fakeDeploymentsHealthReview()[0], nil)
-	k8s.On("GetPods", "ns", "").Return(fakePodsHealthReview(), nil)
-	k8s.On("GetProxyStatus").Return([]*kubernetes.ProxyStatus{}, nil)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
 	prom.MockWorkloadRequestRates("ns", "reviews-v1", otherRatesIn, otherRatesOut)
 
-	hs := HealthService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	hs := HealthService{prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
 
 	mockWorkload := models.Workload{}
 	mockWorkload.Name = "reviews-v1"
@@ -164,23 +163,25 @@ func TestGetWorkloadHealth(t *testing.T) {
 func TestGetAppHealthWithoutIstio(t *testing.T) {
 	assert := assert.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
+	objects := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+		&core_v1.Service{ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "ns"}},
+	}
+	for _, obj := range fakeDeploymentsHealthReview() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	for _, obj := range fakePodsHealthReviewWithoutIstio() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(objects...)
 	prom := new(prometheustest.PromClientMock)
-	conf := config.NewConfig()
-	config.Set(conf)
-
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.MockEmptyWorkloads("ns")
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", "ns").Return(fakeDeploymentsHealthReview(), nil)
-	k8s.On("GetPods", "ns", "app=reviews").Return(fakePodsHealthReviewWithoutIstio(), nil)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
 	prom.MockAppRequestRates("ns", "reviews", otherRatesIn, otherRatesOut)
 
-	hs := HealthService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	hs := HealthService{prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
 
 	mockApp := appDetails{}
 
@@ -194,24 +195,23 @@ func TestGetAppHealthWithoutIstio(t *testing.T) {
 func TestGetWorkloadHealthWithoutIstio(t *testing.T) {
 	assert := assert.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
+	objects := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+		&core_v1.Service{ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "ns"}},
+		&fakeDeploymentsHealthReview()[0],
+	}
+	for _, obj := range fakePodsHealthReviewWithoutIstio() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	k8s.OpenShift = true
 	prom := new(prometheustest.PromClientMock)
-	conf := config.NewConfig()
-	config.Set(conf)
-
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.MockEmptyWorkload("ns", "reviews-v1")
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployment", "ns", "reviews-v1").Return(&fakeDeploymentsHealthReview()[0], nil)
-	k8s.On("GetPods", "ns", "").Return(fakePodsHealthReviewWithoutIstio(), nil)
-	k8s.On("GetWorkload", "ns", "wk", "", false).Return(&models.Workload{}, nil)
 
 	queryTime := time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC)
 	prom.MockWorkloadRequestRates("ns", "reviews-v1", otherRatesIn, otherRatesOut)
 
-	hs := HealthService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	hs := HealthService{prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
 
 	mockWorkload := models.Workload{}
 	mockWorkload.Name = "reviews-v1"
@@ -224,21 +224,27 @@ func TestGetWorkloadHealthWithoutIstio(t *testing.T) {
 }
 
 func TestGetNamespaceAppHealthWithoutIstio(t *testing.T) {
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	prom := new(prometheustest.PromClientMock)
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.MockEmptyWorkloads("ns")
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetServices", "ns", mock.AnythingOfType("map[string]string")).Return([]core_v1.Service{}, nil)
-	k8s.On("GetDeployments", "ns").Return(fakeDeploymentsHealthReview(), nil)
-	k8s.On("GetPods", "ns", "").Return(fakePodsHealthReviewWithoutIstio(), nil)
+	objects := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+		&core_v1.Service{ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "ns"}},
+	}
+	for _, obj := range fakeDeploymentsHealthReview() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	for _, obj := range fakePodsHealthReviewWithoutIstio() {
+		o := obj
+		objects = append(objects, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(objects...)
+	k8s.OpenShift = true
+	prom := new(prometheustest.PromClientMock)
 
-	hs := HealthService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	hs := HealthService{prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
 	criteria := NamespaceHealthCriteria{Namespace: "ns", RateInterval: "1m", QueryTime: time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC), IncludeMetrics: true}
 	_, _ = hs.GetNamespaceAppHealth(context.TODO(), criteria)
 
@@ -249,19 +255,21 @@ func TestGetNamespaceAppHealthWithoutIstio(t *testing.T) {
 func TestGetNamespaceServiceHealthWithNA(t *testing.T) {
 	assert := assert.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
+	reviews := kubetest.FakeService("tutorial", "reviews")
+	httpbin := kubetest.FakeService("tutorial", "httpbin")
+	k8s := kubetest.NewFakeK8sClient(
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "ns"}},
+		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "tutorial"}},
+		&core_v1.Service{ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "ns"}},
+		&reviews,
+		&httpbin,
+	)
+	k8s.OpenShift = true
 	prom := new(prometheustest.PromClientMock)
-	conf := config.NewConfig()
-	config.Set(conf)
 
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.MockServices("tutorial", []string{"reviews", "httpbin"})
 	prom.On("GetNamespaceServicesRequestRates", "tutorial", mock.AnythingOfType("string"), mock.AnythingOfType("time.Time")).Return(serviceRates, nil)
 
-	hs := HealthService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	hs := HealthService{prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
 
 	criteria := NamespaceHealthCriteria{Namespace: "tutorial", RateInterval: "1m", QueryTime: time.Date(2017, 1, 15, 0, 0, 0, 0, time.UTC), IncludeMetrics: true}
 	health, err := hs.GetNamespaceServiceHealth(context.TODO(), criteria)
@@ -408,6 +416,7 @@ func fakePodsHealthReview() []core_v1.Pod {
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:        "reviews-v1",
+				Namespace:   "ns",
 				Labels:      map[string]string{"app": "reviews", "version": "v1"},
 				Annotations: kubetest.FakeIstioAnnotations(),
 			},
@@ -415,6 +424,7 @@ func fakePodsHealthReview() []core_v1.Pod {
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:        "reviews-v2",
+				Namespace:   "ns",
 				Labels:      map[string]string{"app": "reviews", "version": "v2"},
 				Annotations: kubetest.FakeIstioAnnotations(),
 			},
@@ -426,14 +436,16 @@ func fakePodsHealthReviewWithoutIstio() []core_v1.Pod {
 	return []core_v1.Pod{
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
-				Name:   "reviews-v1",
-				Labels: map[string]string{"app": "reviews", "version": "v1"},
+				Name:      "reviews-v1",
+				Namespace: "ns",
+				Labels:    map[string]string{"app": "reviews", "version": "v1"},
 			},
 		},
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
-				Name:   "reviews-v2",
-				Labels: map[string]string{"app": "reviews", "version": "v2"},
+				Name:      "reviews-v2",
+				Namespace: "ns",
+				Labels:    map[string]string{"app": "reviews", "version": "v2"},
 			},
 		},
 	}
@@ -443,7 +455,8 @@ func fakeDeploymentsHealthReview() []apps_v1.Deployment {
 	return []apps_v1.Deployment{
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
-				Name: "reviews-v1",
+				Name:      "reviews-v1",
+				Namespace: "ns",
 			},
 			Status: apps_v1.DeploymentStatus{
 				Replicas:            3,
@@ -458,7 +471,8 @@ func fakeDeploymentsHealthReview() []apps_v1.Deployment {
 		},
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
-				Name: "reviews-v2",
+				Name:      "reviews-v2",
+				Namespace: "ns",
 			},
 			Status: apps_v1.DeploymentStatus{
 				Replicas:            2,
diff --git a/business/istio_status_test.go b/business/istio_status_test.go
index 3ea4b2dc5..f8887912a 100644
--- a/business/istio_status_test.go
+++ b/business/istio_status_test.go
@@ -189,6 +189,10 @@ func TestGrafanaWorking(t *testing.T) {
 	k8s, httpServ, grafanaCalls, promCalls := mockAddOnsCalls(sampleIstioComponent())
 	defer httpServ.Close()
 
+	conf := config.Get()
+	conf.KubernetesConfig.CacheEnabled = false
+	config.Set(conf)
+
 	iss := NewWithBackends(k8s, nil, mockJaeger).IstioStatus
 	icsl, error := iss.GetStatus(context.TODO())
 	assert.NoError(error)
@@ -212,6 +216,7 @@ func TestGrafanaDisabled(t *testing.T) {
 	// Disable Grafana
 	conf := config.Get()
 	conf.ExternalServices.Grafana.Enabled = false
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 
 	iss := NewWithBackends(k8s, nil, mockJaeger).IstioStatus
@@ -250,6 +255,7 @@ func TestGrafanaNotWorking(t *testing.T) {
 
 	// Adapt the AddOns URLs to the mock Server
 	conf := addonAddMockUrls(httpServer.URL, config.NewConfig(), false)
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 
 	iss := NewWithBackends(k8s, nil, mockJaeger).IstioStatus
@@ -275,6 +281,10 @@ func TestFailingTracingService(t *testing.T) {
 	k8s, httpServ, grafanaCalls, promCalls := mockAddOnsCalls(sampleIstioComponent())
 	defer httpServ.Close()
 
+	conf := config.Get()
+	conf.KubernetesConfig.CacheEnabled = false
+	config.Set(conf)
+
 	iss := NewWithBackends(k8s, nil, mockFailingJaeger).IstioStatus
 	icsl, error := iss.GetStatus(context.TODO())
 	assert.NoError(error)
@@ -292,6 +302,10 @@ func TestFailingTracingService(t *testing.T) {
 func TestOverriddenUrls(t *testing.T) {
 	assert := assert.New(t)
 
+	conf := config.Get()
+	conf.KubernetesConfig.CacheEnabled = false
+	config.Set(conf)
+
 	dps, ds, pods, idReachable, _ := sampleIstioComponent()
 	k8s, httpServ, grafanaCalls, promCalls := mockAddOnsCalls(dps, ds, pods, idReachable, true)
 	defer httpServ.Close()
@@ -820,6 +834,7 @@ func defaultAddOnCalls(grafana, prom *int) map[string]addOnsSetup {
 }
 
 func addonAddMockUrls(baseUrl string, conf *config.Config, overrideUrl bool) *config.Config {
+	conf.KubernetesConfig.CacheEnabled = false
 	conf.ExternalServices.Grafana.Enabled = true
 	conf.ExternalServices.Grafana.InClusterURL = baseUrl + "/grafana/mock"
 	conf.ExternalServices.Grafana.IsCore = false
diff --git a/business/istio_validations_perf_test.go b/business/istio_validations_perf_test.go
index 5252cf191..bc5196944 100644
--- a/business/istio_validations_perf_test.go
+++ b/business/istio_validations_perf_test.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/log"
@@ -18,6 +19,7 @@ import (
 
 func TestGetValidationsPerf(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
 	conf := config.NewConfig()
 	config.Set(conf)
 
@@ -48,7 +50,8 @@ func TestGetValidationsPerf(t *testing.T) {
 		[]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "product2.test.svc.cluster.local", "customer.test.svc.cluster.local"}, "test", fakePods())
 
 	now := time.Now()
-	validations, _ := vs.GetValidations(context.TODO(), "test", "", "")
+	validations, err := vs.GetValidations(context.TODO(), "test", "", "")
+	require.NoError(err)
 	log.Debugf("Validation Performance test took %f seconds for %d namespaces", time.Since(now).Seconds(), numNs)
 	assert.NotEmpty(validations)
 }
diff --git a/business/istio_validations_test.go b/business/istio_validations_test.go
index 7d98ad366..c9fe24ef5 100644
--- a/business/istio_validations_test.go
+++ b/business/istio_validations_test.go
@@ -159,7 +159,7 @@ func mockWorkLoadService(k8s *kubetest.K8SClientMock) WorkloadService {
 	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(fakePods().Items, nil)
 	k8s.On("GetConfigMap", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&core_v1.ConfigMap{}, nil)
 
-	svc := setupWorkloadService(k8s)
+	svc := setupWorkloadService(k8s, config.Get())
 	return svc
 }
 
@@ -245,7 +245,8 @@ func fakeIstioConfigList() *models.IstioConfigList {
 	istioConfigList.VirtualServices = []*networking_v1beta1.VirtualService{
 		data.AddHttpRoutesToVirtualService(data.CreateHttpRouteDestination("product", "v1", -1),
 			data.AddTcpRoutesToVirtualService(data.CreateTcpRoute("product2", "v1", -1),
-				data.CreateEmptyVirtualService("product-vs", "test", []string{"product"})))}
+				data.CreateEmptyVirtualService("product-vs", "test", []string{"product"}))),
+	}
 
 	istioConfigList.DestinationRules = []*networking_v1beta1.DestinationRule{
 		data.AddSubsetToDestinationRule(data.CreateSubset("v1", "v1"), data.CreateEmptyDestinationRule("test", "product-dr", "product")),
@@ -334,7 +335,8 @@ func getGateway(name, namespace string) []*networking_v1beta1.Gateway {
 		data.AddServerToGateway(data.CreateServer([]string{"valid"}, 80, "http", "http"),
 			data.CreateEmptyGateway(name, namespace, map[string]string{
 				"app": "real",
-			}))}
+			})),
+	}
 }
 
 func loadVirtualService(file string, t *testing.T) *networking_v1beta1.VirtualService {
diff --git a/business/mesh_test.go b/business/mesh_test.go
index 6c1a7f54e..e6ca75844 100644
--- a/business/mesh_test.go
+++ b/business/mesh_test.go
@@ -1,7 +1,9 @@
 package business
 
 import (
+	"fmt"
 	"net/http/httptest"
+	"os"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -115,7 +117,6 @@ func TestGetClustersResolvesTheKialiCluster(t *testing.T) {
 	check.True(a[0].IsKialiHome, "Kiali cluster not properly marked as such")
 	check.True(a[0].IsGatewayToNamespace, "Kiali GatewayToNamespace not properly marked as such")
 	check.Equal("http://127.0.0.2:9443", a[0].ApiEndpoint)
-	check.Len(a[0].SecretName, 0)
 	check.Equal("kialiNetwork", a[0].Network)
 
 	check.Len(a[0].KialiInstances, 1, "GetClusters didn't resolve the local Kiali instance")
@@ -127,6 +128,9 @@ func TestGetClustersResolvesTheKialiCluster(t *testing.T) {
 }
 
 func TestGetClustersResolvesRemoteClusters(t *testing.T) {
+	// create a mock volume mount directory where the test remote cluster secret content will go
+	remoteClusterSecretsDir = t.TempDir()
+
 	check := assert.New(t)
 
 	k8s := new(kubetest.K8SClientMock)
@@ -162,23 +166,11 @@ func TestGetClustersResolvesRemoteClusters(t *testing.T) {
 		},
 	}
 	marshalledRemoteSecretData, _ := yaml.Marshal(remoteSecretData)
-
-	secretMock := core_v1.Secret{
-		ObjectMeta: v1.ObjectMeta{
-			Name: "TheRemoteSecret",
-			Annotations: map[string]string{
-				"networking.istio.io/cluster": "KialiCluster",
-			},
-		},
-		Data: map[string][]byte{
-			"KialiCluster": marshalledRemoteSecretData,
-		},
-	}
+	createTestRemoteClusterSecretFile(t, remoteClusterSecretsDir, "KialiCluster", string(marshalledRemoteSecretData))
 
 	var nilDeployment *apps_v1.Deployment
 	k8s.On("IsOpenShift").Return(false)
 	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetSecrets", conf.IstioNamespace, "istio/multiCluster=true").Return([]core_v1.Secret{secretMock}, nil)
 	k8s.On("GetDeployment", conf.IstioNamespace, "istiod").Return(nilDeployment, nil)
 
 	newRemoteClient := func(config *rest.Config) (kubernetes.ClientInterface, error) {
@@ -226,7 +218,6 @@ func TestGetClustersResolvesRemoteClusters(t *testing.T) {
 	check.Equal("KialiCluster", a[0].Name, "Unexpected cluster name")
 	check.False(a[0].IsKialiHome, "Remote cluster mistakenly marked as the Kiali home")
 	check.Equal("https://192.168.144.17:123", a[0].ApiEndpoint)
-	check.Equal("TheRemoteSecret", a[0].SecretName)
 	check.Equal("TheRemoteNetwork", a[0].Network)
 
 	check.Len(a[0].KialiInstances, 1, "GetClusters didn't resolve the remote Kiali instance")
@@ -237,6 +228,22 @@ func TestGetClustersResolvesRemoteClusters(t *testing.T) {
 	check.Equal("kiali-service", a[0].KialiInstances[0].ServiceName, "GetClusters didn't set the right service name of the Kiali instance")
 }
 
+func createTestRemoteClusterSecretFile(t *testing.T, parentDir string, name string, content string) {
+	childDir := fmt.Sprintf("%s/%s", parentDir, name)
+	filename := fmt.Sprintf("%s/%s", childDir, name)
+	if err := os.MkdirAll(childDir, 0777); err != nil {
+		t.Fatalf("Failed to create tmp remote cluster secret dir [%v]: %v", childDir, err)
+	}
+	f, err := os.Create(filename)
+	if err != nil {
+		t.Fatalf("Failed to create tmp remote cluster secret file [%v]: %v", filename, err)
+	}
+	defer f.Close()
+	if _, err2 := f.WriteString(content); err2 != nil {
+		t.Fatalf("Failed to write tmp remote cluster secret file [%v]: %v", filename, err2)
+	}
+}
+
 // TestIsMeshConfiguredIsCached verifies that IsMeshConfigured is properly caching
 // it's findings and the cached value is being returned.
 func TestIsMeshConfiguredIsCached(t *testing.T) {
diff --git a/business/references/k8s_gateway_references_test.go b/business/references/k8s_gateway_references_test.go
new file mode 100644
index 000000000..86d709bb8
--- /dev/null
+++ b/business/references/k8s_gateway_references_test.go
@@ -0,0 +1,55 @@
+package references
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/tests/data"
+)
+
+func TestK8sGatewayReferences(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	gw := data.CreateEmptyK8sGateway("bookinfo", "bookinfo")
+
+	r1 := data.CreateHTTPRoute("details", "bookinfo", gw.Name, []string{})
+	r2 := data.CreateEmptyHTTPRoute("httpbin", "default", []string{})
+
+	gatewayReferences := K8sGatewayReferences{
+		K8sGateways:   []*k8s_networking_v1beta1.Gateway{gw},
+		K8sHTTPRoutes: []*k8s_networking_v1beta1.HTTPRoute{r1, r2},
+	}
+
+	references := gatewayReferences.References()
+	gateway := references[models.IstioReferenceKey{ObjectType: "k8sgateway", Namespace: "bookinfo", Name: "bookinfo"}]
+
+	assert.Len(gateway.ObjectReferences, 1)
+	assert.Equal(gateway.ObjectReferences[0].Name, "details")
+	assert.Equal(gateway.ObjectReferences[0].Namespace, "bookinfo")
+}
+
+func TestK8sGatewayNoReferences(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	gw := data.CreateEmptyK8sGateway("bookinfo", "bookinfo")
+
+	r := data.CreateEmptyHTTPRoute("httpbin", "default", []string{})
+
+	gatewayReferences := K8sGatewayReferences{
+		K8sGateways:   []*k8s_networking_v1beta1.Gateway{gw},
+		K8sHTTPRoutes: []*k8s_networking_v1beta1.HTTPRoute{r},
+	}
+
+	references := gatewayReferences.References()
+	gateway := references[models.IstioReferenceKey{ObjectType: "k8sgateway", Namespace: "bookinfo", Name: "bookinfo"}]
+
+	assert.Empty(gateway.ObjectReferences)
+}
diff --git a/business/references/k8shttproute_references_test.go b/business/references/k8shttproute_references_test.go
new file mode 100644
index 000000000..2e52aa13d
--- /dev/null
+++ b/business/references/k8shttproute_references_test.go
@@ -0,0 +1,58 @@
+package references
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/tests/data"
+)
+
+func prepareTestForK8sHTTPRoute(route *k8s_networking_v1beta1.HTTPRoute) models.IstioReferences {
+	routeReferences := K8sHTTPRouteReferences{
+		Namespaces: models.Namespaces{
+			{Name: "bookinfo"},
+			{Name: "bookinfo2"},
+			{Name: "bookinfo3"},
+		},
+		K8sHTTPRoutes: []*k8s_networking_v1beta1.HTTPRoute{route},
+	}
+	return *routeReferences.References()[models.IstioReferenceKey{ObjectType: "k8shttproute", Namespace: route.Namespace, Name: route.Name}]
+}
+
+func TestK8sHTTPRouteReferences(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	// Setup mocks
+	references := prepareTestForK8sHTTPRoute(data.AddBackendRefToHTTPRoute("reviews2", "bookinfo2", data.AddBackendRefToHTTPRoute("reviews", "bookinfo", data.CreateHTTPRoute("route1", "bookinfo", "gatewayapi", []string{"bookinfo"}))))
+	assert.NotEmpty(references.ServiceReferences)
+
+	// Check Service references
+	assert.Len(references.ServiceReferences, 2)
+	assert.Equal(references.ServiceReferences[0].Name, "reviews")
+	assert.Equal(references.ServiceReferences[0].Namespace, "bookinfo")
+	assert.Equal(references.ServiceReferences[1].Name, "reviews2")
+	assert.Equal(references.ServiceReferences[1].Namespace, "bookinfo2")
+
+	assert.Len(references.ObjectReferences, 1)
+	// Check Gateway references
+	assert.Equal(references.ObjectReferences[0].Name, "gatewayapi")
+	assert.Equal(references.ObjectReferences[0].Namespace, "bookinfo")
+	assert.Equal(references.ObjectReferences[0].ObjectType, "k8sgateway")
+}
+
+func TestK8sHTTPRouteNoReferences(t *testing.T) {
+	assert := assert.New(t)
+	conf := config.NewConfig()
+	config.Set(conf)
+
+	// Setup mocks
+	references := prepareTestForK8sHTTPRoute(data.CreateEmptyHTTPRoute("route1", "bookinfo", []string{"details"}))
+	assert.Empty(references.ServiceReferences)
+}
diff --git a/business/test_util.go b/business/test_util.go
index 89d2b555a..bbb60bde1 100644
--- a/business/test_util.go
+++ b/business/test_util.go
@@ -17,7 +17,6 @@ import (
 
 func FakeDeployments() []apps_v1.Deployment {
 	conf := config.NewConfig()
-	config.Set(conf)
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -28,6 +27,7 @@ func FakeDeployments() []apps_v1.Deployment {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.DeploymentSpec{
@@ -49,6 +49,7 @@ func FakeDeployments() []apps_v1.Deployment {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v2",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.DeploymentSpec{
@@ -70,6 +71,7 @@ func FakeDeployments() []apps_v1.Deployment {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v3",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.DeploymentSpec{
@@ -90,7 +92,7 @@ func FakeDeployments() []apps_v1.Deployment {
 
 func FakeDuplicatedDeployments() []apps_v1.Deployment {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -121,7 +123,7 @@ func FakeDuplicatedDeployments() []apps_v1.Deployment {
 
 func FakeReplicaSets() []apps_v1.ReplicaSet {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -132,6 +134,7 @@ func FakeReplicaSets() []apps_v1.ReplicaSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.ReplicaSetSpec{
@@ -153,6 +156,7 @@ func FakeReplicaSets() []apps_v1.ReplicaSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v2",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.ReplicaSetSpec{
@@ -174,6 +178,7 @@ func FakeReplicaSets() []apps_v1.ReplicaSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v3",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.ReplicaSetSpec{
@@ -194,7 +199,7 @@ func FakeReplicaSets() []apps_v1.ReplicaSet {
 
 func FakeDuplicatedReplicaSets() []apps_v1.ReplicaSet {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -205,7 +210,8 @@ func FakeDuplicatedReplicaSets() []apps_v1.ReplicaSet {
 				Kind: "ReplicaSet",
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
-				Name:              "duplicated-v1-12345",
+				Name: "duplicated-v1-12345",
+
 				CreationTimestamp: meta_v1.NewTime(t1),
 				OwnerReferences: []meta_v1.OwnerReference{{
 					Controller: &controller,
@@ -233,7 +239,7 @@ func FakeReplicationControllers() []core_v1.ReplicationController {
 	conf := config.NewConfig()
 	// Enable ReplicationController, those are not fetched by default
 	conf.KubernetesConfig.ExcludeWorkloads = []string{}
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -244,6 +250,7 @@ func FakeReplicationControllers() []core_v1.ReplicationController {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: core_v1.ReplicationControllerSpec{
@@ -265,6 +272,7 @@ func FakeReplicationControllers() []core_v1.ReplicationController {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v2",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: core_v1.ReplicationControllerSpec{
@@ -286,6 +294,7 @@ func FakeReplicationControllers() []core_v1.ReplicationController {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v3",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: core_v1.ReplicationControllerSpec{
@@ -307,7 +316,7 @@ func FakeReplicationControllers() []core_v1.ReplicationController {
 func FakeDeploymentConfigs() []osapps_v1.DeploymentConfig {
 	conf := config.NewConfig()
 	conf.KubernetesConfig.ExcludeWorkloads = []string{}
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -318,6 +327,7 @@ func FakeDeploymentConfigs() []osapps_v1.DeploymentConfig {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: osapps_v1.DeploymentConfigSpec{
@@ -339,6 +349,7 @@ func FakeDeploymentConfigs() []osapps_v1.DeploymentConfig {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v2",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: osapps_v1.DeploymentConfigSpec{
@@ -360,6 +371,7 @@ func FakeDeploymentConfigs() []osapps_v1.DeploymentConfig {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v3",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: osapps_v1.DeploymentConfigSpec{
@@ -381,7 +393,7 @@ func FakeDeploymentConfigs() []osapps_v1.DeploymentConfig {
 func FakeStatefulSets() []apps_v1.StatefulSet {
 	conf := config.NewConfig()
 	conf.KubernetesConfig.ExcludeWorkloads = []string{}
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -392,6 +404,7 @@ func FakeStatefulSets() []apps_v1.StatefulSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.StatefulSetSpec{
@@ -412,6 +425,7 @@ func FakeStatefulSets() []apps_v1.StatefulSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v2",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.StatefulSetSpec{
@@ -432,6 +446,7 @@ func FakeStatefulSets() []apps_v1.StatefulSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v3",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.StatefulSetSpec{
@@ -452,7 +467,7 @@ func FakeStatefulSets() []apps_v1.StatefulSet {
 func FakeDaemonSets() []apps_v1.DaemonSet {
 	conf := config.NewConfig()
 	conf.KubernetesConfig.ExcludeWorkloads = []string{}
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -463,6 +478,7 @@ func FakeDaemonSets() []apps_v1.DaemonSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.DaemonSetSpec{
@@ -484,6 +500,7 @@ func FakeDaemonSets() []apps_v1.DaemonSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v2",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.DaemonSetSpec{
@@ -505,6 +522,7 @@ func FakeDaemonSets() []apps_v1.DaemonSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "httpbin-v3",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.DaemonSetSpec{
@@ -525,7 +543,7 @@ func FakeDaemonSets() []apps_v1.DaemonSet {
 
 func FakeDuplicatedStatefulSets() []apps_v1.StatefulSet {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -536,6 +554,7 @@ func FakeDuplicatedStatefulSets() []apps_v1.StatefulSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "duplicated-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.StatefulSetSpec{
@@ -555,7 +574,7 @@ func FakeDuplicatedStatefulSets() []apps_v1.StatefulSet {
 
 func FakeDepSyncedWithRS() []apps_v1.Deployment {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -566,6 +585,7 @@ func FakeDepSyncedWithRS() []apps_v1.Deployment {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "details-v1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 			},
 			Spec: apps_v1.DeploymentSpec{
@@ -586,7 +606,7 @@ func FakeDepSyncedWithRS() []apps_v1.Deployment {
 
 func FakeRSSyncedWithPods() []apps_v1.ReplicaSet {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -598,6 +618,7 @@ func FakeRSSyncedWithPods() []apps_v1.ReplicaSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "details-v1-3618568057",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 				OwnerReferences: []meta_v1.OwnerReference{{
 					Controller: &controller,
@@ -623,7 +644,7 @@ func FakeRSSyncedWithPods() []apps_v1.ReplicaSet {
 
 func FakePodsSyncedWithDeployments() []core_v1.Pod {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -632,6 +653,7 @@ func FakePodsSyncedWithDeployments() []core_v1.Pod {
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "details-v1-3618568057-dnkjp",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 				Labels:            map[string]string{appLabel: "httpbin", versionLabel: "v1"},
 				OwnerReferences: []meta_v1.OwnerReference{{
@@ -657,7 +679,7 @@ func FakePodsSyncedWithDeployments() []core_v1.Pod {
 
 func FakePodSyncedWithDeployments() *core_v1.Pod {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -665,6 +687,7 @@ func FakePodSyncedWithDeployments() *core_v1.Pod {
 	return &core_v1.Pod{
 		ObjectMeta: meta_v1.ObjectMeta{
 			Name:              "details-v1-3618568057-dnkjp",
+			Namespace:         "Namespace",
 			CreationTimestamp: meta_v1.NewTime(t1),
 			Labels:            map[string]string{appLabel: "httpbin", versionLabel: "v1"},
 			OwnerReferences: []meta_v1.OwnerReference{{
@@ -704,7 +727,7 @@ func FakePodLogsProxy() *kubernetes.PodLogs {
 
 func FakePodsSyncedWithDuplicated() []core_v1.Pod {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -713,6 +736,7 @@ func FakePodsSyncedWithDuplicated() []core_v1.Pod {
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "duplicated-v1-3618568057-1",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 				Labels:            map[string]string{appLabel: "duplicated", versionLabel: "v1"},
 				OwnerReferences: []meta_v1.OwnerReference{{
@@ -736,6 +760,7 @@ func FakePodsSyncedWithDuplicated() []core_v1.Pod {
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "duplicated-v1-3618568057-3",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 				Labels:            map[string]string{appLabel: "duplicated", versionLabel: "v1"},
 				OwnerReferences: []meta_v1.OwnerReference{{
@@ -761,7 +786,7 @@ func FakePodsSyncedWithDuplicated() []core_v1.Pod {
 
 func FakePodsNoController() []core_v1.Pod {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -773,6 +798,7 @@ func FakePodsNoController() []core_v1.Pod {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "orphan-pod",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 				Labels:            map[string]string{appLabel: "httpbin", versionLabel: "v1"},
 				Annotations:       kubetest.FakeIstioAnnotations(),
@@ -793,7 +819,7 @@ func FakePodsNoController() []core_v1.Pod {
 
 func FakePodsFromCustomController() []core_v1.Pod {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -802,6 +828,7 @@ func FakePodsFromCustomController() []core_v1.Pod {
 		{
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "custom-controller-pod",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 				Labels:            map[string]string{appLabel: "httpbin", versionLabel: "v1"},
 				OwnerReferences: []meta_v1.OwnerReference{{
@@ -827,7 +854,7 @@ func FakePodsFromCustomController() []core_v1.Pod {
 
 func FakeCustomControllerRSSyncedWithPods() []apps_v1.ReplicaSet {
 	conf := config.NewConfig()
-	config.Set(conf)
+
 	appLabel := conf.IstioLabels.AppLabelName
 	versionLabel := conf.IstioLabels.VersionLabelName
 	t1, _ := time.Parse(time.RFC822Z, "08 Mar 18 17:44 +0300")
@@ -839,6 +866,7 @@ func FakeCustomControllerRSSyncedWithPods() []apps_v1.ReplicaSet {
 			},
 			ObjectMeta: meta_v1.ObjectMeta{
 				Name:              "custom-controller-RS-123",
+				Namespace:         "Namespace",
 				CreationTimestamp: meta_v1.NewTime(t1),
 				OwnerReferences: []meta_v1.OwnerReference{{
 					Controller: &controller,
@@ -865,7 +893,7 @@ func FakeCustomControllerRSSyncedWithPods() []apps_v1.ReplicaSet {
 func FakeServices() []core_v1.Service {
 	return []core_v1.Service{
 		{
-			ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin"},
+			ObjectMeta: meta_v1.ObjectMeta{Name: "httpbin", Namespace: "Namespace"},
 			Spec: core_v1.ServiceSpec{
 				Selector: map[string]string{"app": "httpbin"},
 			},
diff --git a/business/workloads_test.go b/business/workloads_test.go
index 350257195..a89bc32cd 100644
--- a/business/workloads_test.go
+++ b/business/workloads_test.go
@@ -3,23 +3,18 @@ package business
 import (
 	"context"
 	"encoding/json"
-	"fmt"
 	"io"
 	"net/http/httptest"
 	"strings"
 	"testing"
 	"time"
 
-	osapps_v1 "github.com/openshift/api/apps/v1"
 	osproject_v1 "github.com/openshift/api/project/v1"
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-	apps_v1 "k8s.io/api/apps/v1"
-	batch_v1 "k8s.io/api/batch/v1"
+	"github.com/stretchr/testify/require"
 	core_v1 "k8s.io/api/core/v1"
-	errors "k8s.io/apimachinery/pkg/api/errors"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 
 	"github.com/kiali/kiali/config"
@@ -29,9 +24,12 @@ import (
 	"github.com/kiali/kiali/prometheus/prometheustest"
 )
 
-func setupWorkloadService(k8s *kubetest.K8SClientMock) WorkloadService {
+func setupWorkloadService(k8s kubernetes.ClientInterface, conf *config.Config) WorkloadService {
+	// config needs to be set by other services since those rely on the global.
+	conf.KubernetesConfig.CacheEnabled = false
+	config.Set(conf)
 	prom := new(prometheustest.PromClientMock)
-	return WorkloadService{k8s: k8s, prom: prom, businessLayer: NewWithBackends(k8s, prom, nil)}
+	return *NewWorkloadService(k8s, prom, nil, NewWithBackends(k8s, prom, nil), conf)
 }
 
 func callStreamPodLogs(svc WorkloadService, namespace, podName string, opts *LogOptions) PodLog {
@@ -49,27 +47,19 @@ func callStreamPodLogs(svc WorkloadService, namespace, podName string, opts *Log
 
 func TestGetWorkloadListFromDeployments(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeDeployments(), nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.Pod{}, nil)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(core_v1.Pod{}, nil)
-	k8s.On("GetPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(&kubernetes.PodLogs{}, nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeDeployments() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -77,7 +67,7 @@ func TestGetWorkloadListFromDeployments(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(3, len(workloads))
+	require.Equal(3, len(workloads))
 	assert.Equal("httpbin-v1", workloads[0].Name)
 	assert.Equal(true, workloads[0].AppLabel)
 	assert.Equal(false, workloads[0].VersionLabel)
@@ -94,27 +84,19 @@ func TestGetWorkloadListFromDeployments(t *testing.T) {
 
 func TestGetWorkloadListFromReplicaSets(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeReplicaSets(), nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.Pod{}, nil)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(core_v1.Pod{}, nil)
-	k8s.On("GetPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(&kubernetes.PodLogs{}, nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeReplicaSets() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -122,7 +104,7 @@ func TestGetWorkloadListFromReplicaSets(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(3, len(workloads))
+	require.Equal(3, len(workloads))
 	assert.Equal("httpbin-v1", workloads[0].Name)
 	assert.Equal(true, workloads[0].AppLabel)
 	assert.Equal(false, workloads[0].VersionLabel)
@@ -139,23 +121,19 @@ func TestGetWorkloadListFromReplicaSets(t *testing.T) {
 
 func TestGetWorkloadListFromReplicationControllers(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeReplicationControllers(), nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.Pod{}, nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeReplicationControllers() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	excludedWorkloads = map[string]bool{}
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
@@ -164,7 +142,7 @@ func TestGetWorkloadListFromReplicationControllers(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(3, len(workloads))
+	require.Equal(3, len(workloads))
 	assert.Equal("httpbin-v1", workloads[0].Name)
 	assert.Equal(true, workloads[0].AppLabel)
 	assert.Equal(false, workloads[0].VersionLabel)
@@ -181,25 +159,19 @@ func TestGetWorkloadListFromReplicationControllers(t *testing.T) {
 
 func TestGetWorkloadListFromDeploymentConfigs(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeDeploymentConfigs(), nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.Pod{}, nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeDeploymentConfigs() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	excludedWorkloads = map[string]bool{}
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
@@ -208,7 +180,7 @@ func TestGetWorkloadListFromDeploymentConfigs(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(3, len(workloads))
+	require.Equal(3, len(workloads))
 	assert.Equal("httpbin-v1", workloads[0].Name)
 	assert.Equal(true, workloads[0].AppLabel)
 	assert.Equal(false, workloads[0].VersionLabel)
@@ -225,25 +197,19 @@ func TestGetWorkloadListFromDeploymentConfigs(t *testing.T) {
 
 func TestGetWorkloadListFromStatefulSets(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeStatefulSets(), nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.Pod{}, nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeStatefulSets() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	excludedWorkloads = map[string]bool{}
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
@@ -252,7 +218,7 @@ func TestGetWorkloadListFromStatefulSets(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(3, len(workloads))
+	require.Equal(3, len(workloads))
 	assert.Equal("httpbin-v1", workloads[0].Name)
 	assert.Equal(true, workloads[0].AppLabel)
 	assert.Equal(false, workloads[0].VersionLabel)
@@ -269,25 +235,19 @@ func TestGetWorkloadListFromStatefulSets(t *testing.T) {
 
 func TestGetWorkloadListFromDaemonSets(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeDaemonSets(), nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.Pod{}, nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeDaemonSets() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	excludedWorkloads = map[string]bool{}
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
@@ -296,7 +256,7 @@ func TestGetWorkloadListFromDaemonSets(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(3, len(workloads))
+	require.Equal(3, len(workloads))
 	assert.Equal("httpbin-v1", workloads[0].Name)
 	assert.Equal(true, workloads[0].AppLabel)
 	assert.Equal(false, workloads[0].VersionLabel)
@@ -313,25 +273,27 @@ func TestGetWorkloadListFromDaemonSets(t *testing.T) {
 
 func TestGetWorkloadListFromDepRCPod(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeDepSyncedWithRS(), nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeRSSyncedWithPods(), nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsSyncedWithDeployments(), nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeDepSyncedWithRS() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakeRSSyncedWithPods() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakePodsSyncedWithDeployments() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -339,7 +301,7 @@ func TestGetWorkloadListFromDepRCPod(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(1, len(workloads))
+	require.Equal(1, len(workloads))
 	assert.Equal("details-v1", workloads[0].Name)
 	assert.Equal("Deployment", workloads[0].Type)
 	assert.Equal(true, workloads[0].AppLabel)
@@ -348,25 +310,19 @@ func TestGetWorkloadListFromDepRCPod(t *testing.T) {
 
 func TestGetWorkloadListFromPod(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsNoController(), nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakePodsNoController() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -374,7 +330,7 @@ func TestGetWorkloadListFromPod(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(1, len(workloads))
+	require.Equal(1, len(workloads))
 	assert.Equal("orphan-pod", workloads[0].Name)
 	assert.Equal("Pod", workloads[0].Type)
 	assert.Equal(true, workloads[0].AppLabel)
@@ -383,25 +339,23 @@ func TestGetWorkloadListFromPod(t *testing.T) {
 
 func TestGetWorkloadListFromPods(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeCustomControllerRSSyncedWithPods(), nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsFromCustomController(), nil)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeCustomControllerRSSyncedWithPods() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakePodsFromCustomController() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -409,7 +363,7 @@ func TestGetWorkloadListFromPods(t *testing.T) {
 
 	assert.Equal("Namespace", workloadList.Namespace.Name)
 
-	assert.Equal(1, len(workloads))
+	require.Equal(1, len(workloads))
 	assert.Equal("custom-controller-RS-123", workloads[0].Name)
 	assert.Equal("ReplicaSet", workloads[0].Type)
 	assert.Equal(true, workloads[0].AppLabel)
@@ -418,35 +372,30 @@ func TestGetWorkloadListFromPods(t *testing.T) {
 
 func TestGetWorkloadFromDeployment(t *testing.T) {
 	assert := assert.New(t)
-
-	// Setup mocks
-	gr := schema.GroupResource{
-		Group:    "test-group",
-		Resource: "test-resource",
-	}
-	notfound := errors.NewNotFound(gr, "not found")
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}}, nil)
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&FakeDepSyncedWithRS()[0], nil)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeRSSyncedWithPods(), nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.StatefulSet{}, notfound)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsSyncedWithDeployments(), nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
+	require := require.New(t)
 
 	// Disabling CustomDashboards on Workload details testing
-	conf := config.Get()
+	conf := config.NewConfig()
 	conf.ExternalServices.CustomDashboards.Enabled = false
-	config.Set(conf)
 
-	svc := setupWorkloadService(k8s)
+	// Setup mocks
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+		&FakeDepSyncedWithRS()[0],
+	}
+	for _, o := range FakeRSSyncedWithPods() {
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, o := range FakePodsSyncedWithDeployments() {
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, conf)
+
 	criteria := WorkloadCriteria{Namespace: "Namespace", WorkloadName: "details-v1", WorkloadType: "", IncludeServices: false}
-	workload, _ := svc.GetWorkload(context.TODO(), criteria)
+	workload, err := svc.GetWorkload(context.TODO(), criteria)
+	require.NoError(err)
 
 	assert.Equal("details-v1", workload.Name)
 	assert.Equal("Deployment", workload.Type)
@@ -456,36 +405,32 @@ func TestGetWorkloadFromDeployment(t *testing.T) {
 
 func TestGetWorkloadWithInvalidWorkloadType(t *testing.T) {
 	assert := assert.New(t)
-
-	// Setup mocks
-	gr := schema.GroupResource{
-		Group:    "test-group",
-		Resource: "test-resource",
-	}
-	notfound := errors.NewNotFound(gr, "not found")
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&FakeDepSyncedWithRS()[0], nil)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeRSSyncedWithPods(), nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.StatefulSet{}, notfound)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsSyncedWithDeployments(), nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
+	require := require.New(t)
 
 	// Disabling CustomDashboards on Workload details testing
-	conf := config.Get()
+	conf := config.NewConfig()
 	conf.ExternalServices.CustomDashboards.Enabled = false
-	config.Set(conf)
 
-	svc := setupWorkloadService(k8s)
+	// Setup mocks
+	kubeObjs := []runtime.Object{
+		&FakeDepSyncedWithRS()[0],
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeRSSyncedWithPods() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakePodsSyncedWithDeployments() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, conf)
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", WorkloadName: "details-v1", WorkloadType: "invalid", IncludeServices: false}
-	workload, _ := svc.GetWorkload(context.TODO(), criteria)
+	workload, err := svc.GetWorkload(context.TODO(), criteria)
+	require.NoError(err)
 
 	assert.Equal("details-v1", workload.Name)
 	assert.Equal("Deployment", workload.Type)
@@ -495,42 +440,38 @@ func TestGetWorkloadWithInvalidWorkloadType(t *testing.T) {
 
 func TestGetWorkloadFromPods(t *testing.T) {
 	assert := assert.New(t)
-
-	// Setup mocks
-	gr := schema.GroupResource{
-		Group:    "test-group",
-		Resource: "test-resource",
-	}
-	notfound := errors.NewNotFound(gr, "not found")
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.Deployment{}, notfound)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeCustomControllerRSSyncedWithPods(), nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.StatefulSet{}, notfound)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsFromCustomController(), nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
+	require := require.New(t)
 
 	// Disabling CustomDashboards on Workload details testing
-	conf := config.Get()
+	conf := config.NewConfig()
 	conf.ExternalServices.CustomDashboards.Enabled = false
-	config.Set(conf)
 
-	svc := setupWorkloadService(k8s)
+	// Setup mocks
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeCustomControllerRSSyncedWithPods() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakePodsFromCustomController() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, conf)
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", WorkloadName: "custom-controller", WorkloadType: "", IncludeServices: false}
-	workload, _ := svc.GetWorkload(context.TODO(), criteria)
+	workload, err := svc.GetWorkload(context.TODO(), criteria)
+	require.Error(err)
 
 	// custom controller is not a workload type, only its replica set(s)
 	assert.Equal((*models.Workload)(nil), workload)
 
 	criteria = WorkloadCriteria{Namespace: "Namespace", WorkloadName: "custom-controller-RS-123", WorkloadType: "", IncludeServices: false}
-	workload, _ = svc.GetWorkload(context.TODO(), criteria)
+	workload, err = svc.GetWorkload(context.TODO(), criteria)
+	require.NoError(err)
 
 	assert.Equal("custom-controller-RS-123", workload.Name)
 	assert.Equal("ReplicaSet", workload.Type)
@@ -542,57 +483,62 @@ func TestGetWorkloadFromPods(t *testing.T) {
 
 func TestGetPods(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsSyncedWithDeployments(), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakePodsSyncedWithDeployments() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	pods, _ := svc.GetPods(context.TODO(), "Namespace", "app=httpbin")
 
-	assert.Equal(1, len(pods))
+	require.Equal(1, len(pods))
 	assert.Equal("details-v1-3618568057-dnkjp", pods[0].Name)
 }
 
 func TestGetPod(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodSyncedWithDeployments(), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
+	k8s := kubetest.NewFakeK8sClient(FakePodSyncedWithDeployments(), &osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}})
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
-	svc := setupWorkloadService(k8s)
-
-	pod, _ := svc.GetPod("Namespace", "details-v1-3618568057-dnkjp")
+	pod, err := svc.GetPod("Namespace", "details-v1-3618568057-dnkjp")
+	require.NoError(err)
 
 	assert.Equal("details-v1-3618568057-dnkjp", pod.Name)
 }
 
+// a fake log streamer that returns a fixed string for testing.
+type logStreamer struct {
+	logs string
+	kubernetes.ClientInterface
+}
+
+func (l *logStreamer) StreamPodLogs(namespace, name string, opts *core_v1.PodLogOptions) (io.ReadCloser, error) {
+	return io.NopCloser(strings.NewReader(l.logs)), nil
+}
+
 func TestGetPodLogs(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	fplswd := FakePodLogsSyncedWithDeployments()
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplswd.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
+	k8s := &logStreamer{
+		logs:            FakePodLogsSyncedWithDeployments().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}}),
+	}
 
-	svc := setupWorkloadService(k8s)
+	svc := setupWorkloadService(k8s, config.NewConfig())
 	podLogs := callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{PodLogOptions: core_v1.PodLogOptions{Container: "details"}})
 
-	assert.Equal(len(podLogs.Entries), 4)
+	require.Equal(len(podLogs.Entries), 4)
 
 	assert.Equal("2018-01-02 03:34:28.000", podLogs.Entries[0].Timestamp)
 	assert.Equal(int64(1514864068000), podLogs.Entries[0].TimestampUnix)
@@ -617,68 +563,64 @@ func TestGetPodLogs(t *testing.T) {
 
 func TestGetPodLogsMaxLines(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
+	require := require.New(t)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	fplswd := FakePodLogsSyncedWithDeployments()
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplswd.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
+	k8s := &logStreamer{
+		logs:            FakePodLogsSyncedWithDeployments().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}}),
+	}
 
-	svc := setupWorkloadService(k8s)
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	maxLines := 2
 	duration, _ := time.ParseDuration("6h")
 	podLogs := callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{PodLogOptions: core_v1.PodLogOptions{Container: "details"}, MaxLines: &maxLines, Duration: &duration})
 
-	assert.Equal(2, len(podLogs.Entries))
+	require.Equal(2, len(podLogs.Entries))
 	assert.Equal("INFO #1 Log Message", podLogs.Entries[0].Message)
 	assert.Equal("WARN #2 Log Message", podLogs.Entries[1].Message)
 }
 
 func TestGetPodLogsDuration(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
 	conf := config.NewConfig()
-	config.Set(conf)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	fplswd := FakePodLogsSyncedWithDeployments()
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplswd.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-	svc := setupWorkloadService(k8s)
+	proj := &osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}}
+	k8s := &logStreamer{
+		logs:            FakePodLogsSyncedWithDeployments().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(proj),
+	}
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	duration, _ := time.ParseDuration("59m")
 	podLogs := callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{PodLogOptions: core_v1.PodLogOptions{Container: "details"}, Duration: &duration})
-	assert.Equal(1, len(podLogs.Entries))
+	require.Equal(1, len(podLogs.Entries))
 	assert.Equal("INFO #1 Log Message", podLogs.Entries[0].Message)
 
 	// Re-setup mocks
-	k8s = new(kubetest.K8SClientMock)
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplswd.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-	svc = setupWorkloadService(k8s)
+	k8s = &logStreamer{
+		logs:            FakePodLogsSyncedWithDeployments().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(proj),
+	}
+	svc = setupWorkloadService(k8s, conf)
 
 	duration, _ = time.ParseDuration("1h")
 	podLogs = callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{PodLogOptions: core_v1.PodLogOptions{Container: "details"}, Duration: &duration})
-	assert.Equal(2, len(podLogs.Entries))
+	require.Equal(2, len(podLogs.Entries))
 	assert.Equal("INFO #1 Log Message", podLogs.Entries[0].Message)
 	assert.Equal("WARN #2 Log Message", podLogs.Entries[1].Message)
 
 	// Re-setup mocks
-	k8s = new(kubetest.K8SClientMock)
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplswd.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-	svc = setupWorkloadService(k8s)
+	k8s = &logStreamer{
+		logs:            FakePodLogsSyncedWithDeployments().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(proj),
+	}
+	svc = setupWorkloadService(k8s, conf)
 
 	duration, _ = time.ParseDuration("2h")
 	podLogs = callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{PodLogOptions: core_v1.PodLogOptions{Container: "details"}, Duration: &duration})
-	assert.Equal(3, len(podLogs.Entries))
+	require.Equal(3, len(podLogs.Entries))
 	assert.Equal("INFO #1 Log Message", podLogs.Entries[0].Message)
 	assert.Equal("WARN #2 Log Message", podLogs.Entries[1].Message)
 	assert.Equal("#3 Log Message", podLogs.Entries[2].Message)
@@ -686,36 +628,36 @@ func TestGetPodLogsDuration(t *testing.T) {
 
 func TestGetPodLogsMaxLinesAndDurations(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
 	conf := config.NewConfig()
-	config.Set(conf)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	fplswd := FakePodLogsSyncedWithDeployments()
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplswd.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-	svc := setupWorkloadService(k8s)
+	proj := &osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}}
+	k8s := &logStreamer{
+		logs:            FakePodLogsSyncedWithDeployments().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(proj),
+	}
+	svc := setupWorkloadService(k8s, conf)
 
 	maxLines := 2
 	duration, _ := time.ParseDuration("2h")
 	podLogs := callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{Duration: &duration, PodLogOptions: core_v1.PodLogOptions{Container: "details"}, MaxLines: &maxLines})
-	assert.Equal(2, len(podLogs.Entries))
+	require.Equal(2, len(podLogs.Entries))
 	assert.Equal("INFO #1 Log Message", podLogs.Entries[0].Message)
 	assert.Equal("WARN #2 Log Message", podLogs.Entries[1].Message)
 	assert.True(podLogs.LinesTruncated)
 
 	// Re-setup mocks
-	k8s = new(kubetest.K8SClientMock)
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplswd.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-	svc = setupWorkloadService(k8s)
+	k8s = &logStreamer{
+		logs:            FakePodLogsSyncedWithDeployments().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(proj),
+	}
+	svc = setupWorkloadService(k8s, conf)
 
 	maxLines = 3
 	duration, _ = time.ParseDuration("3h")
 	podLogs = callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{Duration: &duration, PodLogOptions: core_v1.PodLogOptions{Container: "details"}, MaxLines: &maxLines})
-	assert.Equal(3, len(podLogs.Entries))
+	require.Equal(3, len(podLogs.Entries))
 	assert.Equal("INFO #1 Log Message", podLogs.Entries[0].Message)
 	assert.Equal("WARN #2 Log Message", podLogs.Entries[1].Message)
 	assert.Equal("#3 Log Message", podLogs.Entries[2].Message)
@@ -724,22 +666,21 @@ func TestGetPodLogsMaxLinesAndDurations(t *testing.T) {
 
 func TestGetPodLogsProxy(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
 	conf := config.NewConfig()
-	config.Set(conf)
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	fplp := FakePodLogsProxy()
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(fplp.Logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-
-	svc := setupWorkloadService(k8s)
+	proj := &osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}}
+	k8s := &logStreamer{
+		logs:            FakePodLogsProxy().Logs,
+		ClientInterface: kubetest.NewFakeK8sClient(proj),
+	}
+	svc := setupWorkloadService(k8s, conf)
 
 	maxLines := 2
 	duration, _ := time.ParseDuration("2h")
 	podLogs := callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{Duration: &duration, IsProxy: true, PodLogOptions: core_v1.PodLogOptions{Container: "details"}, MaxLines: &maxLines})
-	assert.Equal(1, len(podLogs.Entries))
+	require.Equal(1, len(podLogs.Entries))
 	entry := podLogs.Entries[0]
 	assert.Equal(`[2021-02-01T21:34:35.533Z] "GET /hotels/Ljubljana HTTP/1.1" 200 - via_upstream - "-" 0 99 14 14 "-" "Go-http-client/1.1" "7e7e2dd0-0a96-4535-950b-e303805b7e27" "hotels.travel-agency:8000" "127.0.2021-02-01T21:34:38.761055140Z 0.1:8000" inbound|8000|| 127.0.0.1:33704 10.129.0.72:8000 10.128.0.79:39880 outbound_.8000_._.hotels.travel-agency.svc.cluster.local default`, entry.Message)
 	assert.Equal("2021-02-01 21:34:35.533", entry.Timestamp)
@@ -752,49 +693,51 @@ func TestGetPodLogsProxy(t *testing.T) {
 
 func TestDuplicatedControllers(t *testing.T) {
 	assert := assert.New(t)
-
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeDuplicatedDeployments(), nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeDuplicatedReplicaSets(), nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakeDuplicatedStatefulSets(), nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodsSyncedWithDuplicated(), nil)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(FakePodSyncedWithDeployments(), nil)
-	k8s.On("GetPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(FakePodLogsSyncedWithDeployments(), nil)
-
-	notfound := fmt.Errorf("not found")
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&FakeDuplicatedDeployments()[0], nil)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&FakeDuplicatedStatefulSets()[0], nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
+	require := require.New(t)
 
 	// Disabling CustomDashboards on Workload details testing
-	conf := config.Get()
+	conf := config.NewConfig()
 	conf.ExternalServices.CustomDashboards.Enabled = false
-	config.Set(conf)
 
-	svc := setupWorkloadService(k8s)
+	// Setup mocks
+	kubeObjs := []runtime.Object{
+		FakePodSyncedWithDeployments(),
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range FakeDuplicatedDeployments() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakeDuplicatedReplicaSets() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakeDuplicatedStatefulSets() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range FakePodsSyncedWithDuplicated() {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, conf)
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
 	workloads := workloadList.Workloads
 
 	criteria = WorkloadCriteria{Namespace: "Namespace", WorkloadName: "duplicated-v1", WorkloadType: "", IncludeServices: false}
-	workload, _ := svc.GetWorkload(context.TODO(), criteria)
+	workload, err := svc.GetWorkload(context.TODO(), criteria)
 
+	require.NoError(err)
 	assert.Equal(workloads[0].Type, workload.Type)
 }
 
 func TestGetWorkloadListFromGenericPodController(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
 
 	pods := FakePodsSyncedWithDeployments()
 
@@ -812,44 +755,32 @@ func TestGetWorkloadListFromGenericPodController(t *testing.T) {
 	}
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods, nil)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods[0], nil)
-	k8s.On("GetPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(pods, nil)
-
-	notfound := fmt.Errorf("not found")
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range pods {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	// Disabling CustomDashboards on Workload details testing
 	conf := config.Get()
 	conf.ExternalServices.CustomDashboards.Enabled = false
 	config.Set(conf)
 
-	svc := setupWorkloadService(k8s)
-
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
 	workloads := workloadList.Workloads
 
 	criteria = WorkloadCriteria{Namespace: "Namespace", WorkloadName: owner.Name, WorkloadType: "", IncludeServices: false}
-	workload, _ := svc.GetWorkload(context.TODO(), criteria)
+	workload, err := svc.GetWorkload(context.TODO(), criteria)
 
-	assert.Equal(len(workloads), 1)
-	assert.NotNil(workload)
+	require.NoError(err)
+	require.Equal(len(workloads), 1)
+	require.NotNil(workload)
 
 	assert.Equal(len(pods), len(workload.Pods))
 }
@@ -857,40 +788,30 @@ func TestGetWorkloadListFromGenericPodController(t *testing.T) {
 func TestGetWorkloadListKindsWithSameName(t *testing.T) {
 	assert := assert.New(t)
 
+	// Disabling CustomDashboards on Workload details testing
+	conf := config.NewConfig()
+	conf.ExternalServices.CustomDashboards.Enabled = false
+
 	rs := FakeRSSyncedWithPods()
 	pods := FakePodsSyncedWithDeployments()
 	pods[0].OwnerReferences[0].APIVersion = "shiny.new.apps/v1"
 	pods[0].OwnerReferences[0].Kind = "ReplicaSet"
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(rs, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods, nil)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods[0], nil)
-	k8s.On("GetPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(pods, nil)
-
-	notfound := fmt.Errorf("not found")
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
-
-	// Disabling CustomDashboards on Workload details testing
-	conf := config.Get()
-	conf.ExternalServices.CustomDashboards.Enabled = false
-	config.Set(conf)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range rs {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range pods {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, conf)
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -902,6 +823,10 @@ func TestGetWorkloadListKindsWithSameName(t *testing.T) {
 func TestGetWorkloadListRSWithoutPrefix(t *testing.T) {
 	assert := assert.New(t)
 
+	// Disabling CustomDashboards on Workload details testing
+	conf := config.NewConfig()
+	conf.ExternalServices.CustomDashboards.Enabled = false
+
 	rs := FakeRSSyncedWithPods()
 	// Doesn't matter what the type is as long as kiali doesn't recognize it as a workload.
 	owner := &core_v1.ConfigMap{
@@ -918,34 +843,20 @@ func TestGetWorkloadListRSWithoutPrefix(t *testing.T) {
 	pods := FakePodsSyncedWithDeployments()
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(rs, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods, nil)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods[0], nil)
-	k8s.On("GetPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(pods, nil)
-
-	notfound := fmt.Errorf("not found")
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
-
-	// Disabling CustomDashboards on Workload details testing
-	conf := config.Get()
-	conf.ExternalServices.CustomDashboards.Enabled = false
-	config.Set(conf)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range rs {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range pods {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, conf)
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -956,6 +867,11 @@ func TestGetWorkloadListRSWithoutPrefix(t *testing.T) {
 
 func TestGetWorkloadListRSOwnedByCustom(t *testing.T) {
 	assert := assert.New(t)
+	require := require.New(t)
+
+	// Disabling CustomDashboards on Workload details testing
+	conf := config.NewConfig()
+	conf.ExternalServices.CustomDashboards.Enabled = false
 
 	replicaSets := FakeRSSyncedWithPods()
 
@@ -978,34 +894,20 @@ func TestGetWorkloadListRSOwnedByCustom(t *testing.T) {
 	pods := FakePodsSyncedWithDeployments()
 
 	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
-	k8s.On("IsOpenShift").Return(true)
-	k8s.On("IsGatewayAPI").Return(false)
-	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
-	k8s.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfigs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]osapps_v1.DeploymentConfig{}, nil)
-	k8s.On("GetReplicaSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(replicaSets, nil)
-	k8s.On("GetReplicationControllers", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
-	k8s.On("GetStatefulSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
-	k8s.On("GetJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.Job{}, nil)
-	k8s.On("GetCronJobs", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]batch_v1.CronJob{}, nil)
-	k8s.On("GetPods", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods, nil)
-	k8s.On("GetPod", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(pods[0], nil)
-	k8s.On("GetPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(FakePodsSyncedWithDeployments(), nil)
-
-	notfound := fmt.Errorf("not found")
-	k8s.On("GetDeployment", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.Deployment{}, nil)
-	k8s.On("GetDeploymentConfig", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&osapps_v1.DeploymentConfig{}, notfound)
-	k8s.On("GetStatefulSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.StatefulSet{}, nil)
-	k8s.On("GetDaemonSets", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	k8s.On("GetDaemonSet", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&apps_v1.DaemonSet{}, notfound)
-
-	// Disabling CustomDashboards on Workload details testing
-	conf := config.Get()
-	conf.ExternalServices.CustomDashboards.Enabled = false
-	config.Set(conf)
-
-	svc := setupWorkloadService(k8s)
+	kubeObjs := []runtime.Object{
+		&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}},
+	}
+	for _, obj := range replicaSets {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	for _, obj := range pods {
+		o := obj
+		kubeObjs = append(kubeObjs, &o)
+	}
+	k8s := kubetest.NewFakeK8sClient(kubeObjs...)
+	k8s.OpenShift = true
+	svc := setupWorkloadService(k8s, conf)
 
 	criteria := WorkloadCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
 	workloadList, _ := svc.GetWorkloadList(context.TODO(), criteria)
@@ -1014,7 +916,7 @@ func TestGetWorkloadListRSOwnedByCustom(t *testing.T) {
 	criteria = WorkloadCriteria{Namespace: "Namespace", WorkloadName: owner.Name, WorkloadType: "", IncludeServices: false}
 	workload, _ := svc.GetWorkload(context.TODO(), criteria)
 
-	assert.Equal(len(workloads), 1)
+	require.Equal(len(workloads), 1)
 	assert.Nil(workload)
 
 	criteria.WorkloadName = workloads[0].Name
@@ -1025,11 +927,7 @@ func TestGetWorkloadListRSOwnedByCustom(t *testing.T) {
 
 func TestGetPodLogsWithoutAccessLogs(t *testing.T) {
 	assert := assert.New(t)
-	conf := config.NewConfig()
-	config.Set(conf)
 
-	// Setup mocks
-	k8s := new(kubetest.K8SClientMock)
 	const logs = `2021-10-05T00:32:40.309334Z     debug   envoy http      [C57][S7648448766062793478] request end stream
 2021-10-05T00:32:40.309425Z     debug   envoy router    [C57][S7648448766062793478] cluster 'inbound|9080||' match for URL '/details/0'
 2021-10-05T00:32:40.309438Z     debug   envoy upstream  Using existing host 172.17.0.12:9080.
@@ -1038,11 +936,11 @@ func TestGetPodLogsWithoutAccessLogs(t *testing.T) {
 2021-10-05T00:32:40.309457Z     ':path', '/details/0'
 2021-10-05T00:32:40.309457Z     ':method', 'GET'
 2021-10-05T00:32:40.309457Z     ':scheme', 'http'`
-	k8s.On("StreamPodLogs", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.Anything).Return(io.NopCloser(strings.NewReader(logs)), nil)
-	k8s.On("IsOpenShift").Return(false)
-	k8s.On("IsGatewayAPI").Return(false)
-
-	svc := setupWorkloadService(k8s)
+	k8s := &logStreamer{
+		logs:            logs,
+		ClientInterface: kubetest.NewFakeK8sClient(&osproject_v1.Project{ObjectMeta: v1.ObjectMeta{Name: "Namespace"}}),
+	}
+	svc := setupWorkloadService(k8s, config.NewConfig())
 
 	podLogs := callStreamPodLogs(svc, "Namespace", "details-v1-3618568057-dnkjp", &LogOptions{IsProxy: true, PodLogOptions: core_v1.PodLogOptions{Container: "istio-proxy"}})
 
diff --git a/config/config_test.go b/config/config_test.go
index 5978f37eb..17f65d344 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -249,16 +249,6 @@ func TestRaces(t *testing.T) {
 	wg.Wait()
 }
 
-func TestMarshalUnmarshalCompatibilityMatrix(t *testing.T) {
-	matrix, err := NewCompatibilityMatrix()
-	if err != nil {
-		t.Errorf("Failed to marshal: %v", err)
-	}
-
-	fmt.Printf("%+v", matrix)
-	t.Logf("Config from compatibility matrix file: %+v", matrix)
-}
-
 func TestAllNamespacesAccessible(t *testing.T) {
 	cases := map[string]struct {
 		expectedAccessible   bool
diff --git a/graph/telemetry/istio/appender/dead_node_test.go b/graph/telemetry/istio/appender/dead_node_test.go
index 01c2bc899..3f7740feb 100644
--- a/graph/telemetry/istio/appender/dead_node_test.go
+++ b/graph/telemetry/istio/appender/dead_node_test.go
@@ -21,6 +21,7 @@ import (
 func setupWorkloads() *business.Layer {
 	k8s := kubetest.NewK8SClientMock()
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 
 	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
@@ -86,7 +87,8 @@ func setupWorkloads() *business.Layer {
 					Labels: map[string]string{"app": "testPodsWithTraffic", "version": "v1"},
 				},
 				Status: core_v1.PodStatus{
-					Message: "foo"},
+					Message: "foo",
+				},
 			},
 			{
 				ObjectMeta: meta_v1.ObjectMeta{
@@ -94,14 +96,14 @@ func setupWorkloads() *business.Layer {
 					Labels: map[string]string{"app": "testPodsNoTraffic", "version": "v1"},
 				},
 				Status: core_v1.PodStatus{
-					Message: "foo"},
+					Message: "foo",
+				},
 			},
 		}, nil)
 	k8s.On("GetReplicationControllers", mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
 	k8s.On("GetReplicaSets", mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
 	k8s.On("GetStatefulSets", mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
 	k8s.On("GetDaemonSets", mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	config.Set(config.NewConfig())
 
 	businessLayer := business.NewWithBackends(k8s, nil, nil)
 	return businessLayer
diff --git a/graph/telemetry/istio/appender/health_test.go b/graph/telemetry/istio/appender/health_test.go
index 27a4bcde0..74599ee3d 100644
--- a/graph/telemetry/istio/appender/health_test.go
+++ b/graph/telemetry/istio/appender/health_test.go
@@ -48,7 +48,9 @@ func TestServicesHealthConfigPasses(t *testing.T) {
 }
 
 func TestServicesHealthNoConfigPasses(t *testing.T) {
-	config.Set(config.NewConfig())
+	cfg := config.NewConfig()
+	cfg.KubernetesConfig.CacheEnabled = false
+	config.Set(cfg)
 	trafficMap := buildServiceTrafficMap()
 	businessLayer := setupHealthConfig(buildFakeServicesHealth(""), buildFakeWorkloadDeploymentsHealth(""), buildFakePodsHealth(""))
 
@@ -67,7 +69,9 @@ func TestServicesHealthNoConfigPasses(t *testing.T) {
 }
 
 func TestWorkloadHealthConfigPasses(t *testing.T) {
-	config.Set(config.NewConfig())
+	cfg := config.NewConfig()
+	cfg.KubernetesConfig.CacheEnabled = false
+	config.Set(cfg)
 	trafficMap := buildWorkloadTrafficMap()
 	businessLayer := setupHealthConfig(buildFakeServicesHealth(rateDefinition), buildFakeWorkloadDeploymentsHealth(rateWorkloadDefinition), buildFakePodsHealth(rateWorkloadDefinition))
 
@@ -86,7 +90,9 @@ func TestWorkloadHealthConfigPasses(t *testing.T) {
 }
 
 func TestWorkloadHealthNoConfigPasses(t *testing.T) {
-	config.Set(config.NewConfig())
+	cfg := config.NewConfig()
+	cfg.KubernetesConfig.CacheEnabled = false
+	config.Set(cfg)
 	trafficMap := buildWorkloadTrafficMap()
 	businessLayer := setupHealthConfig(buildFakeServicesHealth(""), buildFakeWorkloadDeploymentsHealth(""), buildFakePodsHealth(""))
 
@@ -107,7 +113,9 @@ func TestWorkloadHealthNoConfigPasses(t *testing.T) {
 func TestHealthDataPresent(t *testing.T) {
 	assert := assert.New(t)
 
-	config.Set(config.NewConfig())
+	cfg := config.NewConfig()
+	cfg.KubernetesConfig.CacheEnabled = false
+	config.Set(cfg)
 	svcNodes := buildServiceTrafficMap()
 	appNodes := buildAppTrafficMap()
 	wkNodes := buildWorkloadTrafficMap()
@@ -138,7 +146,6 @@ func TestHealthDataPresent(t *testing.T) {
 func TestHealthDataPresent200SvcWk(t *testing.T) {
 	assert := assert.New(t)
 
-	config.Set(config.NewConfig())
 	svcNodes := buildServiceTrafficMap()
 	appNodes := buildAppTrafficMap()
 	wkNodes := buildWorkloadTrafficMap()
@@ -454,7 +461,9 @@ func TestIdleNodesHaveHealthData(t *testing.T) {
 func TestErrorCausesPanic(t *testing.T) {
 	assert := assert.New(t)
 
-	config.Set(config.NewConfig())
+	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
+	config.Set(conf)
 	trafficMap := buildAppTrafficMap()
 	k8s := kubetest.NewK8SClientMock()
 	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
@@ -469,7 +478,6 @@ func TestErrorCausesPanic(t *testing.T) {
 	k8s.On("GetDaemonSets", mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
 	const panicErrMsg = "test error! This should cause a panic"
 	k8s.On("GetServices", mock.AnythingOfType("string"), mock.Anything).Return([]core_v1.Service{}, fmt.Errorf(panicErrMsg))
-	config.Set(config.NewConfig())
 	business.SetKialiControlPlaneCluster(&business.Cluster{
 		Name: business.DefaultClusterID,
 	})
@@ -533,7 +541,9 @@ func setupHealthConfig(services []core_v1.Service, deployments []apps_v1.Deploym
 	k8s.On("GetStatefulSets", mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
 	k8s.On("GetDaemonSets", mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
 	k8s.On("GetServices", mock.AnythingOfType("string"), mock.Anything).Return(services, nil)
-	config.Set(config.NewConfig())
+	cfg := config.Get()
+	cfg.KubernetesConfig.CacheEnabled = false
+	config.Set(cfg)
 	business.SetKialiControlPlaneCluster(&business.Cluster{
 		Name: business.DefaultClusterID,
 	})
diff --git a/graph/telemetry/istio/appender/labeler_test.go b/graph/telemetry/istio/appender/labeler_test.go
index ff091008e..3d20b3238 100644
--- a/graph/telemetry/istio/appender/labeler_test.go
+++ b/graph/telemetry/istio/appender/labeler_test.go
@@ -42,6 +42,7 @@ func setupLabelerTrafficMap() (map[string]*graph.Node, string, string, string, s
 func setupLabelerK8S() *business.Layer {
 	k8s := kubetest.NewK8SClientMock()
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 
 	k8s.On("GetProject", mock.AnythingOfType("string")).Return(&osproject_v1.Project{}, nil)
@@ -84,7 +85,8 @@ func setupLabelerK8S() *business.Layer {
 					Labels: graph.LabelsMetadata{"app": "test", "version": "v1", "datacenter": "east"},
 				},
 				Status: core_v1.PodStatus{
-					Message: "foo"},
+					Message: "foo",
+				},
 			},
 			{
 				ObjectMeta: meta_v1.ObjectMeta{
@@ -92,7 +94,8 @@ func setupLabelerK8S() *business.Layer {
 					Labels: graph.LabelsMetadata{"app": "test", "version": "v2", "datacenter": "west"},
 				},
 				Status: core_v1.PodStatus{
-					Message: "foo"},
+					Message: "foo",
+				},
 			},
 		}, nil)
 	k8s.On("GetReplicationControllers", mock.AnythingOfType("string")).Return([]core_v1.ReplicationController{}, nil)
@@ -100,16 +103,15 @@ func setupLabelerK8S() *business.Layer {
 	k8s.On("GetStatefulSets", mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
 	k8s.On("GetDaemonSets", mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
 
-	k8s.On("GetServices", mock.AnythingOfType("string"), mock.Anything).Return([]core_v1.Service{{
-		ObjectMeta: meta_v1.ObjectMeta{
-			Name:   "test",
-			Labels: graph.LabelsMetadata{"app": "test", "datacenter": "east"},
+	k8s.On("GetServices", mock.AnythingOfType("string"), mock.Anything).Return([]core_v1.Service{
+		{
+			ObjectMeta: meta_v1.ObjectMeta{
+				Name:   "test",
+				Labels: graph.LabelsMetadata{"app": "test", "datacenter": "east"},
+			},
 		},
-	},
 	}, nil)
 
-	config.Set(config.NewConfig())
-
 	businessLayer := business.NewWithBackends(k8s, nil, nil)
 	return businessLayer
 }
diff --git a/graph/telemetry/istio/appender/sidecars_check_test.go b/graph/telemetry/istio/appender/sidecars_check_test.go
index ca69b79c7..0b3f8c2c3 100644
--- a/graph/telemetry/istio/appender/sidecars_check_test.go
+++ b/graph/telemetry/istio/appender/sidecars_check_test.go
@@ -20,7 +20,6 @@ import (
 )
 
 func TestWorkloadSidecarsPasses(t *testing.T) {
-	config.Set(config.NewConfig())
 	trafficMap := buildWorkloadTrafficMap()
 	businessLayer := setupSidecarsCheckWorkloads(buildFakeWorkloadDeployments(), buildFakeWorkloadPods())
 
@@ -40,7 +39,6 @@ func TestWorkloadSidecarsPasses(t *testing.T) {
 }
 
 func TestWorkloadWithMissingSidecarsIsFlagged(t *testing.T) {
-	config.Set(config.NewConfig())
 	trafficMap := buildWorkloadTrafficMap()
 	businessLayer := setupSidecarsCheckWorkloads(buildFakeWorkloadDeployments(), buildFakeWorkloadPodsNoSidecar())
 
@@ -61,7 +59,6 @@ func TestWorkloadWithMissingSidecarsIsFlagged(t *testing.T) {
 }
 
 func TestInaccessibleWorkload(t *testing.T) {
-	config.Set(config.NewConfig())
 	trafficMap := buildInaccessibleWorkloadTrafficMap()
 	businessLayer := setupSidecarsCheckWorkloads(buildFakeWorkloadDeployments(), buildFakeWorkloadPodsNoSidecar())
 
@@ -81,7 +78,6 @@ func TestInaccessibleWorkload(t *testing.T) {
 }
 
 func TestAppNoPodsPasses(t *testing.T) {
-	config.Set(config.NewConfig())
 	trafficMap := buildAppTrafficMap()
 	businessLayer := setupSidecarsCheckWorkloads([]apps_v1.Deployment{}, []core_v1.Pod{})
 
@@ -101,7 +97,6 @@ func TestAppNoPodsPasses(t *testing.T) {
 }
 
 func TestAppSidecarsPasses(t *testing.T) {
-	config.Set(config.NewConfig())
 	trafficMap := buildAppTrafficMap()
 	businessLayer := setupSidecarsCheckWorkloads([]apps_v1.Deployment{}, buildFakeWorkloadPods())
 
@@ -121,7 +116,6 @@ func TestAppSidecarsPasses(t *testing.T) {
 }
 
 func TestAppWithMissingSidecarsIsFlagged(t *testing.T) {
-	config.Set(config.NewConfig())
 	trafficMap := buildAppTrafficMap()
 	businessLayer := setupSidecarsCheckWorkloads([]apps_v1.Deployment{}, buildFakeWorkloadPodsNoSidecar())
 
@@ -252,7 +246,9 @@ func setupSidecarsCheckWorkloads(deployments []apps_v1.Deployment, pods []core_v
 	k8s.On("GetReplicaSets", mock.AnythingOfType("string")).Return([]apps_v1.ReplicaSet{}, nil)
 	k8s.On("GetStatefulSets", mock.AnythingOfType("string")).Return([]apps_v1.StatefulSet{}, nil)
 	k8s.On("GetDaemonSets", mock.AnythingOfType("string")).Return([]apps_v1.DaemonSet{}, nil)
-	config.Set(config.NewConfig())
+	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
+	config.Set(conf)
 
 	businessLayer := business.NewWithBackends(k8s, nil, nil)
 	return businessLayer
diff --git a/handlers/proxy_logging_test.go b/handlers/proxy_logging_test.go
index f95f4bc79..9000e6c87 100644
--- a/handlers/proxy_logging_test.go
+++ b/handlers/proxy_logging_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/gorilla/mux"
 	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/tools/clientcmd/api"
 
 	"github.com/kiali/kiali/business"
@@ -36,6 +37,8 @@ func setupTestLoggingServer(t *testing.T, namespace, pod string) *httptest.Serve
 	k8s.On("IsOpenShift").Return(false)
 	k8s.On("IsGatewayAPI").Return(false)
 	k8s.On("SetProxyLogLevel").Return(nil)
+	var fakePod *corev1.Pod
+	k8s.On("GetPod", namespace, pod).Return(fakePod, nil)
 
 	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
 	business.SetWithBackends(mockClientFactory, nil)
diff --git a/handlers/workloads_test.go b/handlers/workloads_test.go
index 3596657fe..13e5bd243 100644
--- a/handlers/workloads_test.go
+++ b/handlers/workloads_test.go
@@ -50,6 +50,7 @@ func setupWorkloadList() (*httptest.Server, *kubetest.K8SClientMock, *prometheus
 
 func TestWorkloadsEndpoint(t *testing.T) {
 	conf := config.NewConfig()
+	conf.KubernetesConfig.CacheEnabled = false
 	config.Set(conf)
 	ts, k8s, _ := setupWorkloadList()
 	k8s.MockIstio()
diff --git a/kubernetes/cache/cache_test.go b/kubernetes/cache/cache_test.go
index 53832e043..dd5fab1c7 100644
--- a/kubernetes/cache/cache_test.go
+++ b/kubernetes/cache/cache_test.go
@@ -1,156 +1,107 @@
 package cache
 
 import (
-	"regexp"
+	"context"
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
-	istiofake "istio.io/client-go/pkg/clientset/versioned/fake"
-	corev1 "k8s.io/api/core/v1"
+	"github.com/stretchr/testify/require"
+	apps_v1 "k8s.io/api/apps/v1"
+	core_v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	kubefake "k8s.io/client-go/kubernetes/fake"
-	gatewayapifake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake"
 
+	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/kubetest"
 )
 
-func newTestKialiCache(kubeObjects []runtime.Object, istioObjects []runtime.Object, gatewayObjects []runtime.Object) *kialiCacheImpl {
-	kialiCache := &kialiCacheImpl{
-		k8sApi:                kubefake.NewSimpleClientset(kubeObjects...),
-		istioApi:              istiofake.NewSimpleClientset(istioObjects...),
-		gatewayApi:            gatewayapifake.NewSimpleClientset(gatewayObjects...),
-		clusterScoped:         false,
-		stopClusterScopedChan: make(chan struct{}),
-		stopNSChans:           make(map[string]chan struct{}),
-		nsCacheLister:         make(map[string]*cacheLister),
-		stopCacheChan:         make(chan bool),
-	}
-	kialiCache.registryRefreshHandler = NewRegistryHandler(kialiCache.RefreshRegistryStatus)
-	return kialiCache
+// Need to lock the client when we go to check the value of the token
+// but only the tests need this functionality so we can use a fake
+// that has access to the kubeCache's lock and has a getClient() method
+// that returns the client after locking. Without this, the tests will
+// fail with the race detector enabled.
+type fakeKubeCache struct {
+	*kubeCache
 }
 
-func TestNewKialiCache_isCached(t *testing.T) {
-	assert := assert.New(t)
-
-	kialiCacheImpl := kialiCacheImpl{
-		istioClient:            kubernetes.K8SClient{},
-		refreshDuration:        0,
-		cacheNamespacesRegexps: []regexp.Regexp{*regexp.MustCompile("bookinfo"), *regexp.MustCompile("a.*"), *regexp.MustCompile("galicia")},
-	}
-
-	assert.True(kialiCacheImpl.isCached("bookinfo"))
-	assert.True(kialiCacheImpl.isCached("a"))
-	assert.True(kialiCacheImpl.isCached("abcdefghi"))
-	assert.False(kialiCacheImpl.isCached("b"))
-	assert.False(kialiCacheImpl.isCached("bbcdefghi"))
-	assert.True(kialiCacheImpl.isCached("galicia"))
+func (f *fakeKubeCache) getClient() kubernetes.ClientInterface {
+	f.kubeCache.cacheLock.RLock()
+	defer f.kubeCache.cacheLock.RUnlock()
+	return f.kubeCache.client
 }
 
-func TestClusterScopedCacheStopped(t *testing.T) {
-	assert := assert.New(t)
+func TestClientUpdatedWhenSAClientChanges(t *testing.T) {
+	require := require.New(t)
+	config := config.NewConfig()
 
-	kialiCacheImpl := newTestKialiCache(nil, nil, nil)
-	stopCh := make(chan struct{})
-	kialiCacheImpl.stopClusterScopedChan = stopCh
-	kialiCacheImpl.clusterScoped = true
-	kialiCacheImpl.Refresh("")
+	client := kubetest.NewFakeK8sClient()
+	client.Token = "current-token"
+	clientFactory := kubetest.NewK8SClientFactoryMock(client)
+	k8sCache, err := NewKubeCache(client, *config, emptyHandler)
+	require.NoError(err)
 
-	kialiCacheImpl.Stop()
-	select {
-	case <-time.After(300 * time.Millisecond):
-		assert.Fail("Cache should have been stopped")
-	case <-stopCh:
+	kubeCache := &fakeKubeCache{kubeCache: k8sCache}
+	kialiCache := &kialiCacheImpl{
+		clientRefreshPollingPeriod: time.Millisecond,
+		clientFactory:              clientFactory,
+		KubeCache:                  kubeCache,
 	}
-}
 
-func TestNSScopedCacheStopped(t *testing.T) {
-	assert := assert.New(t)
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
 
-	stopChs := map[string]chan struct{}{
-		"ns1": make(chan struct{}),
-		"ns2": make(chan struct{}),
-	}
-	kialiCacheImpl := newTestKialiCache(nil, nil, nil)
-	kialiCacheImpl.stopNSChans = stopChs
-	kialiCacheImpl.clusterScoped = false
-
-	kialiCacheImpl.Stop()
-	for ns, stopCh := range stopChs {
-		select {
-		case <-time.After(300 * time.Millisecond):
-			assert.Failf("Cache for namespace: %s should have been stopped", ns)
-		case <-stopCh:
-		}
-	}
-
-	assert.Empty(kialiCacheImpl.nsCacheLister)
-}
+	kialiCache.watchForClientChanges(ctx, client.Token)
 
-func TestRefreshClusterScoped(t *testing.T) {
-	assert := assert.New(t)
+	// Update the client. This should trigger a cache refresh.
+	newClient := kubetest.NewFakeK8sClient()
+	newClient.Token = "new-token"
+	clientFactory.SetClients(map[string]kubernetes.ClientInterface{kubernetes.HomeClusterName: newClient})
 
-	svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "svc1", Namespace: "ns1"}}
-	kialiCache := newTestKialiCache([]runtime.Object{svc}, nil, nil)
-	kialiCache.clusterScoped = true
-	kialiCache.clusterCacheLister = &cacheLister{}
-	oldLister := kialiCache.clusterCacheLister
-	kialiCache.Refresh("")
-	assert.NotEqual(kialiCache.clusterCacheLister, oldLister)
+	require.Eventually(
+		func() bool { return kubeCache.getClient() != client },
+		500*time.Millisecond,
+		5*time.Millisecond,
+		"client and cache should have been updated",
+	)
 }
 
-func TestRefreshMultipleTimesClusterScoped(t *testing.T) {
-	assert := assert.New(t)
+func TestNoHomeClusterReturnsError(t *testing.T) {
+	require := require.New(t)
+	config := config.NewConfig()
 
-	kialiCache := newTestKialiCache(nil, nil, nil)
-	kialiCache.clusterScoped = true
-	kialiCache.clusterCacheLister = &cacheLister{}
-	oldLister := kialiCache.clusterCacheLister
+	client := kubetest.NewFakeK8sClient()
+	clientFactory := kubetest.NewK8SClientFactoryMock(client)
+	clientFactory.SetClients(map[string]kubernetes.ClientInterface{"nothomecluster": client})
 
-	kialiCache.Refresh("")
-	kialiCache.Refresh("")
-	assert.NotEqual(kialiCache.clusterCacheLister, oldLister)
+	_, err := NewKialiCache(clientFactory, *config)
+	require.Error(err, "no home cluster should return an error")
 }
 
-func TestRefreshNSScoped(t *testing.T) {
-	assert := assert.New(t)
-
-	kialiCache := newTestKialiCache(nil, nil, nil)
-	kialiCache.clusterScoped = false
-	kialiCache.nsCacheLister = map[string]*cacheLister{}
-
-	kialiCache.Refresh("ns1")
-	assert.NotEqual(kialiCache.nsCacheLister, map[string]*cacheLister{})
-	assert.Contains(kialiCache.nsCacheLister, "ns1")
-}
-
-func TestCheckNamespaceClusterScoped(t *testing.T) {
-	assert := assert.New(t)
-
-	kialiCache := newTestKialiCache(nil, nil, nil)
-	kialiCache.clusterScoped = true
-
-	// Should always return true for cluster scoped cache.
-	assert.True(kialiCache.CheckNamespace("ns1"))
-}
-
-func TestCheckNamespaceNotIncluded(t *testing.T) {
-	assert := assert.New(t)
-
-	kialiCache := newTestKialiCache(nil, nil, nil)
-	kialiCache.clusterScoped = false
-
-	assert.False(kialiCache.CheckNamespace("ns1"))
-}
-
-func TestCheckNamespaceIsIncluded(t *testing.T) {
-	assert := assert.New(t)
-
-	regex := regexp.MustCompile("ns.*")
-	kialiCache := newTestKialiCache(nil, nil, nil)
-	kialiCache.clusterScoped = false
-	kialiCache.cacheNamespacesRegexps = []regexp.Regexp{*regex}
-
-	assert.True(kialiCache.CheckNamespace("ns1"))
+func TestKubeCacheCreatedPerClient(t *testing.T) {
+	require := require.New(t)
+	config := config.NewConfig()
+
+	ns := &core_v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
+	deploymentCluster1 := &apps_v1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deployment1", Namespace: "test"}}
+	deploymentCluster2 := &apps_v1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "deployment2", Namespace: "test"}}
+	client := kubetest.NewFakeK8sClient(ns, deploymentCluster1)
+	client2 := kubetest.NewFakeK8sClient(ns, deploymentCluster2)
+	clientFactory := kubetest.NewK8SClientFactoryMock(nil)
+	clientFactory.SetClients(map[string]kubernetes.ClientInterface{
+		kubernetes.HomeClusterName: client,
+		"cluster2":                 client2,
+	})
+
+	kialiCache, err := NewKialiCache(clientFactory, *config)
+	require.NoError(err)
+	defer kialiCache.Stop()
+
+	caches := kialiCache.GetKubeCaches()
+	require.Equal(2, len(caches))
+
+	_, err = caches[kubernetes.HomeClusterName].GetDeployment("test", "deployment1")
+	require.NoError(err)
+
+	_, err = caches["cluster2"].GetDeployment("test", "deployment2")
+	require.NoError(err)
 }
diff --git a/kubernetes/cache/istio_test.go b/kubernetes/cache/istio_test.go
deleted file mode 100644
index 96f57e322..000000000
--- a/kubernetes/cache/istio_test.go
+++ /dev/null
@@ -1,140 +0,0 @@
-package cache
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
-	istiofake "istio.io/client-go/pkg/clientset/versioned/fake"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	kubefake "k8s.io/client-go/kubernetes/fake"
-
-	"github.com/kiali/kiali/kubernetes"
-)
-
-func TestGetSidecar(t *testing.T) {
-	sidecar := &networking_v1beta1.Sidecar{}
-	sidecar.Name = "moto-sidecar"
-	sidecar.Namespace = "testing-ns"
-	sidecar.Labels = map[string]string{
-		"app":     "bookinfo",
-		"version": "v1",
-	}
-
-	kialiCacheImpl := kialiCacheImpl{
-		clusterScoped:         true,
-		stopClusterScopedChan: make(chan struct{}),
-		istioApi:              istiofake.NewSimpleClientset(sidecar),
-		k8sApi:                kubefake.NewSimpleClientset(),
-		cacheIstioTypes: map[string]bool{
-			kubernetes.PluralType[kubernetes.Sidecars]: true,
-		},
-		clusterCacheLister: &cacheLister{},
-	}
-	kialiCacheImpl.registryRefreshHandler = NewRegistryHandler(kialiCacheImpl.RefreshRegistryStatus)
-
-	cases := map[string]struct {
-		selector        string
-		resourceType    string
-		namespace       string
-		expectedErr     error
-		expectedObjects []*networking_v1beta1.Sidecar
-	}{
-		"With selector that matches": {
-			selector:        "app=bookinfo",
-			resourceType:    kubernetes.Sidecars,
-			expectedErr:     nil,
-			expectedObjects: []*networking_v1beta1.Sidecar{sidecar},
-		},
-		"With selector that doesn't match": {
-			selector:        "app=anotherapp",
-			resourceType:    kubernetes.Sidecars,
-			expectedErr:     nil,
-			expectedObjects: []*networking_v1beta1.Sidecar{},
-		},
-		"Without selector": {
-			resourceType:    kubernetes.Sidecars,
-			expectedErr:     nil,
-			expectedObjects: []*networking_v1beta1.Sidecar{sidecar},
-		},
-		"With unparseable selector": {
-			selector:        "unpar$ablestr!ng!",
-			resourceType:    kubernetes.Sidecars,
-			expectedErr:     fmt.Errorf("Any"),
-			expectedObjects: []*networking_v1beta1.Sidecar{},
-		},
-		"With unknown type": {
-			selector:        "unpar$ablestr!ng!",
-			resourceType:    "unknowntype",
-			expectedErr:     fmt.Errorf("Any"),
-			expectedObjects: []*networking_v1beta1.Sidecar{},
-		},
-		"Uncached namespace returns empty": {
-			namespace:       "uncachednamespace",
-			resourceType:    kubernetes.Sidecars,
-			expectedErr:     nil,
-			expectedObjects: []*networking_v1beta1.Sidecar{},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			assert := assert.New(t)
-			kialiCacheImpl.Refresh(tc.namespace)
-
-			namespace := sidecar.Namespace
-			if tc.namespace != "" {
-				namespace = tc.namespace
-			}
-
-			objects, err := kialiCacheImpl.GetSidecars(namespace, tc.selector)
-			if tc.expectedErr != nil {
-				assert.Error(err)
-			} else {
-				assert.NoError(err)
-			}
-			assert.Equal(len(tc.expectedObjects), len(objects))
-		})
-	}
-}
-
-func TestGetNonCachedResource(t *testing.T) {
-	assert := assert.New(t)
-	sidecar := &networking_v1beta1.VirtualService{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: "sidecar", Namespace: "test", Labels: map[string]string{"app": "bookinfo", "version": "v1"},
-		},
-	}
-	kialiCache := newTestKialiCache(nil, []runtime.Object{sidecar}, nil)
-	kialiCache.cacheIstioTypes = nil
-	_, err := kialiCache.GetVirtualServices("testing-ns", "app=bookinfo")
-	assert.Error(err)
-}
-
-// Other parts of the codebase assume that this kind field is present so it's important
-// that the cache sets it.
-func TestGetAndListReturnKindInfo(t *testing.T) {
-	assert := assert.New(t)
-	vs := &networking_v1beta1.VirtualService{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: "vs", Namespace: "test",
-		},
-	}
-	kialiCache := newTestKialiCache(nil, []runtime.Object{vs}, nil)
-	kialiCache.cacheIstioTypes = map[string]bool{
-		kubernetes.PluralType[kubernetes.VirtualServices]: true,
-	}
-	kialiCache.Refresh("test")
-
-	vsFromCache, err := kialiCache.GetVirtualService("test", "vs")
-	assert.NoError(err)
-	assert.Equal(kubernetes.VirtualServiceType, vsFromCache.Kind)
-
-	vsListFromCache, err := kialiCache.GetVirtualServices("test", "")
-	assert.NoError(err)
-	for _, vs := range vsListFromCache {
-		assert.Equal(kubernetes.VirtualServiceType, vs.Kind)
-	}
-}
diff --git a/kubernetes/cache/kube_cache_test.go b/kubernetes/cache/kube_cache_test.go
new file mode 100644
index 000000000..4cb95b471
--- /dev/null
+++ b/kubernetes/cache/kube_cache_test.go
@@ -0,0 +1,342 @@
+package cache
+
+import (
+	"fmt"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	apps_v1 "k8s.io/api/apps/v1"
+	core_v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/kubetest"
+)
+
+func newTestingKubeCache(t *testing.T, cfg *config.Config, objects ...runtime.Object) *kubeCache {
+	t.Helper()
+
+	emptyRefreshHandler := NewRegistryHandler(func() {})
+	kubeCache, err := NewKubeCache(kubetest.NewFakeK8sClient(objects...), *cfg, emptyRefreshHandler, cfg.Deployment.AccessibleNamespaces...)
+	if err != nil {
+		t.Fatalf("Unable to create kube cache for testing. Err: %s", err)
+	}
+	return kubeCache
+}
+
+func TestNewKialiCache_isCached(t *testing.T) {
+	assert := assert.New(t)
+
+	kubeCache := newTestingKubeCache(t, config.NewConfig())
+	kubeCache.refreshDuration = 0
+	kubeCache.cacheNamespacesRegexps = []regexp.Regexp{*regexp.MustCompile("bookinfo"), *regexp.MustCompile("a.*"), *regexp.MustCompile("galicia")}
+
+	assert.True(kubeCache.isCached("bookinfo"))
+	assert.True(kubeCache.isCached("a"))
+	assert.True(kubeCache.isCached("abcdefghi"))
+	assert.False(kubeCache.isCached("b"))
+	assert.False(kubeCache.isCached("bbcdefghi"))
+	assert.True(kubeCache.isCached("galicia"))
+}
+
+func TestClusterScopedCacheStopped(t *testing.T) {
+	assert := assert.New(t)
+
+	kubeCache := newTestingKubeCache(t, config.NewConfig())
+
+	kubeCache.Stop()
+	select {
+	case <-time.After(300 * time.Millisecond):
+		assert.Fail("Cache should have been stopped")
+	case <-kubeCache.stopClusterScopedChan:
+	}
+}
+
+func TestNSScopedCacheStopped(t *testing.T) {
+	assert := assert.New(t)
+
+	cfg := config.NewConfig()
+	cfg.Deployment.AccessibleNamespaces = []string{"ns1", "ns2"}
+	kubeCache := newTestingKubeCache(t, cfg)
+
+	kubeCache.Stop()
+	for ns, stopCh := range kubeCache.stopNSChans {
+		select {
+		case <-time.After(300 * time.Millisecond):
+			assert.Failf("Cache for namespace: %s should have been stopped", ns)
+		case <-stopCh:
+		}
+	}
+
+	assert.Empty(kubeCache.nsCacheLister)
+}
+
+func TestRefreshClusterScoped(t *testing.T) {
+	assert := assert.New(t)
+
+	svc := &core_v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "svc1", Namespace: "ns1"}}
+	kialiCache := newTestingKubeCache(t, config.NewConfig(), svc)
+	kialiCache.clusterCacheLister = &cacheLister{}
+	oldLister := kialiCache.clusterCacheLister
+	kialiCache.Refresh("")
+	assert.NotEqual(kialiCache.clusterCacheLister, oldLister)
+}
+
+func TestRefreshMultipleTimesClusterScoped(t *testing.T) {
+	assert := assert.New(t)
+
+	kialiCache := newTestingKubeCache(t, config.NewConfig())
+	kialiCache.clusterCacheLister = &cacheLister{}
+	oldLister := kialiCache.clusterCacheLister
+
+	kialiCache.Refresh("")
+	kialiCache.Refresh("")
+	assert.NotEqual(kialiCache.clusterCacheLister, oldLister)
+}
+
+func TestRefreshNSScoped(t *testing.T) {
+	assert := assert.New(t)
+
+	cfg := config.NewConfig()
+	cfg.Deployment.AccessibleNamespaces = []string{"ns1", "ns2"}
+	kialiCache := newTestingKubeCache(t, cfg)
+	kialiCache.nsCacheLister = map[string]*cacheLister{}
+
+	kialiCache.Refresh("ns1")
+	assert.NotEqual(kialiCache.nsCacheLister, map[string]*cacheLister{})
+	assert.Contains(kialiCache.nsCacheLister, "ns1")
+}
+
+func TestCheckNamespaceClusterScoped(t *testing.T) {
+	assert := assert.New(t)
+
+	kialiCache := newTestingKubeCache(t, config.NewConfig())
+
+	// Should always return true for cluster scoped cache.
+	assert.True(kialiCache.CheckNamespace("ns1"))
+}
+
+func TestCheckNamespaceNotIncluded(t *testing.T) {
+	assert := assert.New(t)
+
+	cfg := config.NewConfig()
+	cfg.Deployment.AccessibleNamespaces = []string{"bookinfo"}
+	cfg.KubernetesConfig.CacheNamespaces = []string{"bookinfo"}
+	kialiCache := newTestingKubeCache(t, cfg)
+
+	assert.False(kialiCache.CheckNamespace("ns1"))
+}
+
+func TestCheckNamespaceIsIncluded(t *testing.T) {
+	assert := assert.New(t)
+
+	cfg := config.NewConfig()
+	cfg.Deployment.AccessibleNamespaces = []string{"ns.*"}
+	kialiCache := newTestingKubeCache(t, cfg)
+
+	assert.True(kialiCache.CheckNamespace("ns1"))
+}
+
+// Other parts of the codebase assume that this kind field is present so it's important
+// that the cache sets it.
+func TestKubeGetAndListReturnKindInfo(t *testing.T) {
+	assert := assert.New(t)
+	ns := &core_v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
+	d := &apps_v1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "deployment", Namespace: "test",
+		},
+	}
+	kialiCache := newTestingKubeCache(t, config.NewConfig(), ns, d)
+	kialiCache.Refresh("test")
+
+	deploymentFromCache, err := kialiCache.GetDeployment("test", "deployment")
+	assert.NoError(err)
+	assert.Equal(kubernetes.DeploymentType, deploymentFromCache.Kind)
+
+	deploymentListFromCache, err := kialiCache.GetDeployments("test")
+	assert.NoError(err)
+	for _, deployment := range deploymentListFromCache {
+		assert.Equal(kubernetes.DeploymentType, deployment.Kind)
+	}
+}
+
+// Tests that when a refresh happens, the new cache must fully load before the
+// new object is returned.
+func TestConcurrentAccessDuringRefresh(t *testing.T) {
+	require := require.New(t)
+	d := &apps_v1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "deployment", Namespace: "test",
+		},
+	}
+
+	kialiCache := newTestingKubeCache(t, config.NewConfig(), d)
+	// Prime the pump with a first Refresh.
+	kialiCache.Refresh("test")
+
+	stop := make(chan bool)
+	go func() {
+		for {
+			select {
+			case <-stop:
+				return
+			default:
+				_, err := kialiCache.GetDeployment(d.Namespace, d.Name)
+				require.NoError(err)
+			}
+		}
+	}()
+
+	kialiCache.Refresh("test")
+	close(stop)
+}
+
+func TestGetSidecar(t *testing.T) {
+	ns := &core_v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "testing-ns"}}
+	sidecar := &networking_v1beta1.Sidecar{}
+	sidecar.Name = "moto-sidecar"
+	sidecar.Namespace = "testing-ns"
+	sidecar.Labels = map[string]string{
+		"app":     "bookinfo",
+		"version": "v1",
+	}
+
+	cfg := config.NewConfig()
+	cfg.KubernetesConfig.CacheIstioTypes = []string{kubernetes.PluralType[kubernetes.Sidecars]}
+
+	kubeCache := newTestingKubeCache(t, cfg, ns, sidecar)
+
+	cases := map[string]struct {
+		selector        string
+		resourceType    string
+		namespace       string
+		expectedErr     error
+		expectedObjects []*networking_v1beta1.Sidecar
+	}{
+		"With selector that matches": {
+			selector:        "app=bookinfo",
+			resourceType:    kubernetes.Sidecars,
+			expectedErr:     nil,
+			expectedObjects: []*networking_v1beta1.Sidecar{sidecar},
+		},
+		"With selector that doesn't match": {
+			selector:        "app=anotherapp",
+			resourceType:    kubernetes.Sidecars,
+			expectedErr:     nil,
+			expectedObjects: []*networking_v1beta1.Sidecar{},
+		},
+		"Without selector": {
+			resourceType:    kubernetes.Sidecars,
+			expectedErr:     nil,
+			expectedObjects: []*networking_v1beta1.Sidecar{sidecar},
+		},
+		"With unparseable selector": {
+			selector:        "unpar$ablestr!ng!",
+			resourceType:    kubernetes.Sidecars,
+			expectedErr:     fmt.Errorf("Any"),
+			expectedObjects: []*networking_v1beta1.Sidecar{},
+		},
+		"With unknown type": {
+			selector:        "unpar$ablestr!ng!",
+			resourceType:    "unknowntype",
+			expectedErr:     fmt.Errorf("Any"),
+			expectedObjects: []*networking_v1beta1.Sidecar{},
+		},
+		"Uncached namespace returns empty": {
+			namespace:       "uncachednamespace",
+			resourceType:    kubernetes.Sidecars,
+			expectedErr:     nil,
+			expectedObjects: []*networking_v1beta1.Sidecar{},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			assert := assert.New(t)
+
+			namespace := sidecar.Namespace
+			if tc.namespace != "" {
+				namespace = tc.namespace
+			}
+
+			objects, err := kubeCache.GetSidecars(namespace, tc.selector)
+			if tc.expectedErr != nil {
+				assert.Error(err)
+			} else {
+				assert.NoError(err)
+			}
+			assert.Equal(len(tc.expectedObjects), len(objects))
+		})
+	}
+}
+
+func TestGetNonCachedResource(t *testing.T) {
+	assert := assert.New(t)
+	ns := &core_v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
+	sidecar := &networking_v1beta1.VirtualService{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "sidecar", Namespace: "test", Labels: map[string]string{"app": "bookinfo", "version": "v1"},
+		},
+	}
+
+	cfg := config.NewConfig()
+	cfg.KubernetesConfig.CacheIstioTypes = nil
+	kialiCache := newTestingKubeCache(t, cfg, ns, sidecar)
+
+	_, err := kialiCache.GetVirtualServices("testing-ns", "app=bookinfo")
+	assert.Error(err)
+}
+
+// Other parts of the codebase assume that this kind field is present so it's important
+// that the cache sets it.
+func TestGetAndListReturnKindInfo(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+	ns := &core_v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
+	vs := &networking_v1beta1.VirtualService{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "vs", Namespace: "test",
+		},
+	}
+
+	cfg := config.NewConfig()
+	kialiCache := newTestingKubeCache(t, cfg, ns, vs)
+
+	vsFromCache, err := kialiCache.GetVirtualService("test", "vs")
+	require.NoError(err)
+	assert.Equal(kubernetes.VirtualServiceType, vsFromCache.Kind)
+
+	vsListFromCache, err := kialiCache.GetVirtualServices("test", "")
+	require.NoError(err)
+	for _, vs := range vsListFromCache {
+		assert.Equal(kubernetes.VirtualServiceType, vs.Kind)
+	}
+}
+
+func TestUpdatingClientRefreshesCache(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+
+	ns := &core_v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
+	pod := &core_v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "test"}}
+
+	cfg := config.NewConfig()
+	kialiCache := newTestingKubeCache(t, cfg, ns, pod)
+	kialiCache.clusterCacheLister = &cacheLister{}
+
+	err := kialiCache.UpdateClient(kubetest.NewFakeK8sClient(ns, pod))
+	require.NoError(err)
+
+	assert.NotEqual(kialiCache.clusterCacheLister, &cacheLister{})
+
+	pods, err := kialiCache.GetPods("test", "")
+	require.NoError(err)
+	require.Len(pods, 1)
+}
diff --git a/kubernetes/cache/kubernetes_test.go b/kubernetes/cache/kubernetes_test.go
deleted file mode 100644
index c1dd04bf7..000000000
--- a/kubernetes/cache/kubernetes_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package cache
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	apps_v1 "k8s.io/api/apps/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-
-	"github.com/kiali/kiali/kubernetes"
-)
-
-// Other parts of the codebase assume that this kind field is present so it's important
-// that the cache sets it.
-func TestKubeGetAndListReturnKindInfo(t *testing.T) {
-	assert := assert.New(t)
-	d := &apps_v1.Deployment{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: "deployment", Namespace: "test",
-		},
-	}
-	kialiCache := newTestKialiCache([]runtime.Object{d}, nil, nil)
-	kialiCache.Refresh("test")
-
-	deploymentFromCache, err := kialiCache.GetDeployment("test", "deployment")
-	assert.NoError(err)
-	assert.Equal(kubernetes.DeploymentType, deploymentFromCache.Kind)
-
-	deploymentListFromCache, err := kialiCache.GetDeployments("test")
-	assert.NoError(err)
-	for _, deployment := range deploymentListFromCache {
-		assert.Equal(kubernetes.DeploymentType, deployment.Kind)
-	}
-}
diff --git a/kubernetes/cache/registry_test_helper.go b/kubernetes/cache/registry_test_helper.go
index 60dff0b02..b7ee7dab5 100644
--- a/kubernetes/cache/registry_test_helper.go
+++ b/kubernetes/cache/registry_test_helper.go
@@ -1,21 +1,34 @@
 package cache
 
 import (
+	"fmt"
 	"time"
 
 	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
 	security_v1beta1 "istio.io/client-go/pkg/apis/security/v1beta1"
 
+	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/kubetest"
 )
 
+var emptyHandler = NewRegistryHandler(func() {})
+
 // Fake KialiCache used for Gateway Scenarios
 // It populates the Namespaces, Informers and Gateway information needed
 func FakeGatewaysKialiCache(gws []*networking_v1beta1.Gateway) KialiCache {
+	cfg := config.Get()
+	cfg.Deployment.AccessibleNamespaces = []string{"bookinfo"}
+	cfg.KubernetesConfig.CacheNamespaces = []string{"test"}
+	cache, err := NewKubeCache(kubetest.NewFakeK8sClient(), *cfg, emptyHandler)
+	if err != nil {
+		panic(fmt.Sprintf("Error creating KialiCache in testing. Err: %v", err))
+	}
 	kialiCacheImpl := kialiCacheImpl{
 		tokenNamespaces: make(map[string]namespaceCache),
 		// ~ long duration for unit testing
 		refreshDuration: time.Hour,
+		KubeCache:       cache,
 	}
 
 	// Populate all Gateways using the Registry
@@ -39,11 +52,21 @@ func FakeServicesKialiCache(rss []*kubernetes.RegistryService,
 	ses []*networking_v1beta1.ServiceEntry,
 	sds []*networking_v1beta1.Sidecar,
 	ras []*security_v1beta1.RequestAuthentication,
-	wes []*networking_v1beta1.WorkloadEntry) KialiCache {
+	wes []*networking_v1beta1.WorkloadEntry,
+) KialiCache {
+	// Tests that use this rely on namespaced scoped caches.
+	cfg := config.Get()
+	cfg.Deployment.AccessibleNamespaces = []string{"bookinfo"}
+	cfg.KubernetesConfig.CacheNamespaces = []string{"test"}
+	cache, err := NewKubeCache(kubetest.NewFakeK8sClient(), *cfg, emptyHandler)
+	if err != nil {
+		panic(fmt.Sprintf("Error creating KialiCache in testing. Err: %v", err))
+	}
 	kialiCacheImpl := kialiCacheImpl{
 		tokenNamespaces: make(map[string]namespaceCache),
 		// ~ long duration for unit testing
 		refreshDuration: time.Hour,
+		KubeCache:       cache,
 	}
 
 	// Populate all DestinationRules using the Registry
diff --git a/kubernetes/client_factory_test.go b/kubernetes/client_factory_test.go
index e2e804ab7..5bd6eb84a 100644
--- a/kubernetes/client_factory_test.go
+++ b/kubernetes/client_factory_test.go
@@ -8,65 +8,72 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd/api"
+
+	"github.com/kiali/kiali/config"
 )
 
 // TestClientExpiration Verify the details that clients expire are correct
 func TestClientExpiration(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+
 	istioConfig := rest.Config{}
-	clientFactory := newClientFactory(&istioConfig)
+	clientFactory, err := newClientFactory(&istioConfig)
+	require.NoError(err)
 
 	// Make sure we are starting off with an empty set of clients
-	assert.Equal(t, 0, clientFactory.getClientsLength())
+	assert.Equal(0, clientFactory.getClientsLength())
 
 	// Create a single initial test clients
 	authInfo := api.NewAuthInfo()
 	authInfo.Token = "foo-token"
-	_, err := clientFactory.getRecycleClient(authInfo, 100*time.Millisecond)
-	if err != nil {
-		assert.Nil(t, err)
-	}
+	_, err = clientFactory.getRecycleClient(authInfo, 100*time.Millisecond)
+	require.NoError(err)
 
 	// Verify we have the client
-	assert.Equal(t, 1, clientFactory.getClientsLength())
+	assert.Equal(1, clientFactory.getClientsLength())
 	_, found := clientFactory.hasClient(authInfo)
-	assert.True(t, found)
+	assert.True(found)
 
 	// Sleep for a bit and add another client
 	time.Sleep(time.Millisecond * 60)
 	authInfo1 := api.NewAuthInfo()
 	authInfo1.Token = "bar-token"
 	_, err = clientFactory.getRecycleClient(authInfo1, 100*time.Millisecond)
-	if err != nil {
-		assert.Nil(t, err)
-	}
+	require.NoError(err)
 
 	// Verify we have both the foo and bar clients
-	assert.Equal(t, 2, clientFactory.getClientsLength())
+	assert.Equal(2, clientFactory.getClientsLength())
 	_, found = clientFactory.hasClient(authInfo)
-	assert.True(t, found)
+	assert.True(found)
 	_, found = clientFactory.hasClient(authInfo1)
-	assert.True(t, found)
+	assert.True(found)
 
 	// Wait for foo to be expired
 	time.Sleep(time.Millisecond * 60)
 	// Verify the client has been removed
-	assert.Equal(t, 1, clientFactory.getClientsLength())
+	assert.Equal(1, clientFactory.getClientsLength())
 	_, found = clientFactory.hasClient(authInfo)
-	assert.False(t, found)
+	assert.False(found)
 	_, found = clientFactory.hasClient(authInfo1)
-	assert.True(t, found)
+	assert.True(found)
 
 	// Wait for bar to be expired
 	time.Sleep(time.Millisecond * 60)
-	assert.Equal(t, 0, clientFactory.getClientsLength())
+	assert.Equal(0, clientFactory.getClientsLength())
 }
 
 // TestConcurrentClientExpiration Verify Concurrent clients are expired correctly
 func TestConcurrentClientExpiration(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+
 	istioConfig := rest.Config{}
-	clientFactory := newClientFactory(&istioConfig)
+	clientFactory, err := newClientFactory(&istioConfig)
+	require.NoError(err)
 	count := 100
 
 	wg := sync.WaitGroup{}
@@ -74,23 +81,23 @@ func TestConcurrentClientExpiration(t *testing.T) {
 
 	for i := 0; i < count; i++ {
 		go func() {
+			defer wg.Done()
 			authInfo := api.NewAuthInfo()
 			authInfo.Token = fmt.Sprintf("%d", rand.Intn(10000000000))
-			if _, err := clientFactory.getRecycleClient(authInfo, 10*time.Millisecond); err != nil {
-				assert.Nil(t, err)
-			}
-			wg.Done()
+			_, innerErr := clientFactory.getRecycleClient(authInfo, 10*time.Millisecond)
+			assert.NoError(innerErr)
 		}()
 	}
 
 	wg.Wait()
 	time.Sleep(3 * time.Second)
 
-	assert.Equal(t, 0, clientFactory.getClientsLength())
+	assert.Equal(0, clientFactory.getClientsLength())
 }
 
 // TestConcurrentClientFactory test Concurrently create ClientFactory
 func TestConcurrentClientFactory(t *testing.T) {
+	assert := assert.New(t)
 	istioConfig := rest.Config{}
 	count := 100
 
@@ -99,10 +106,65 @@ func TestConcurrentClientFactory(t *testing.T) {
 
 	for i := 0; i < count; i++ {
 		go func() {
-			newClientFactory(&istioConfig)
-			wg.Done()
+			defer wg.Done()
+			_, err := newClientFactory(&istioConfig)
+			assert.NoError(err)
 		}()
 	}
 
 	wg.Wait()
 }
+
+func TestSAHomeClientUpdatesWhenKialiTokenChanges(t *testing.T) {
+	assert := assert.New(t)
+	require := require.New(t)
+	kialiConfig := config.NewConfig()
+	config.Set(kialiConfig)
+	t.Cleanup(func() {
+		// Other tests use this global var so we need to reset it.
+		KialiToken = ""
+	})
+
+	tokenRead = time.Now()
+	KialiToken = "current-token"
+
+	restConfig := rest.Config{}
+	clientFactory, err := newClientFactory(&restConfig)
+	require.NoError(err)
+
+	currentClient := clientFactory.GetSAHomeClusterClient()
+	assert.Equal(KialiToken, currentClient.GetToken())
+	assert.Equal(currentClient, clientFactory.GetSAHomeClusterClient())
+
+	KialiToken = "new-token"
+
+	// Assert that the token has changed and the client has changed.
+	newClient := clientFactory.GetSAHomeClusterClient()
+	assert.Equal(KialiToken, newClient.GetToken())
+	assert.NotEqual(currentClient, newClient)
+}
+
+func TestSAClientsUpdateWhenKialiTokenChanges(t *testing.T) {
+	require := require.New(t)
+	kialiConfig := config.NewConfig()
+	config.Set(kialiConfig)
+	t.Cleanup(func() {
+		// Other tests use this global var so we need to reset it.
+		KialiToken = ""
+	})
+
+	tokenRead = time.Now()
+	KialiToken = "current-token"
+
+	restConfig := rest.Config{}
+	clientFactory, err := newClientFactory(&restConfig)
+	require.NoError(err)
+
+	client := clientFactory.GetSAClients()[HomeClusterName]
+	require.Equal(KialiToken, client.GetToken())
+
+	KialiToken = "new-token"
+
+	client = clientFactory.GetSAClients()[HomeClusterName]
+	require.Equal(KialiToken, client.GetToken())
+}
diff --git a/kubernetes/kubetest/fake.go b/kubernetes/kubetest/fake.go
new file mode 100644
index 000000000..4e36659df
--- /dev/null
+++ b/kubernetes/kubetest/fake.go
@@ -0,0 +1,179 @@
+package kubetest
+
+import (
+	"context"
+
+	osapps_v1 "github.com/openshift/api/apps/v1"
+	osproject_v1 "github.com/openshift/api/project/v1"
+	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
+	istio "istio.io/client-go/pkg/clientset/versioned"
+	istiofake "istio.io/client-go/pkg/clientset/versioned/fake"
+	istioscheme "istio.io/client-go/pkg/clientset/versioned/scheme"
+	kubeerrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes"
+	kubefake "k8s.io/client-go/kubernetes/fake"
+	kubescheme "k8s.io/client-go/kubernetes/scheme"
+	gatewayapi "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
+	gatewayapifake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake"
+	gatewayapischeme "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/scheme"
+
+	kialikube "github.com/kiali/kiali/kubernetes"
+)
+
+func isIstioResource(obj runtime.Object) bool {
+	_, _, err := istioscheme.Scheme.ObjectKinds(obj)
+	return err == nil
+}
+
+func isKubeResource(obj runtime.Object) bool {
+	_, _, err := kubescheme.Scheme.ObjectKinds(obj)
+	return err == nil
+}
+
+func isGatewayAPIResource(obj runtime.Object) bool {
+	_, _, err := gatewayapischeme.Scheme.ObjectKinds(obj)
+	return err == nil
+}
+
+func isOpenShiftResource(obj runtime.Object) bool {
+	// We don't use openshift's client-go package where the scheme is located
+	// so just manually checking a few types that Kiali uses here. Not all the
+	// types are covered.
+	switch obj.(type) {
+	case *osapps_v1.DeploymentConfig, *osproject_v1.Project:
+		return true
+	}
+
+	return false
+}
+
+func NewFakeK8sClient(objects ...runtime.Object) *FakeK8sClient {
+	// NOTE: The kube fake client object tracker guesses the resource name based on the Kind.
+	// For a plural resource, it will convert the kind to lowercase and add an "ies" to the end.
+	// In the case of objects like "Gateway" where the plural is actually "Gateways", the conversion
+	// is wrong. The guessing of the resource name only happens with tracker.Add inside of NewSimpleClientset.
+	// If we create the object after creating the clientset, then the tracker will use the correct resource name.
+	var (
+		kubeObjects       []runtime.Object
+		istioObjects      []runtime.Object
+		gatewayapiObjects []runtime.Object
+		istioGateways     []*networking_v1beta1.Gateway
+		deploymentConfigs = make(map[string][]osapps_v1.DeploymentConfig)
+		projects          = []osproject_v1.Project{}
+	)
+
+	for _, obj := range objects {
+		o := obj
+		switch {
+		case isKubeResource(o):
+			kubeObjects = append(kubeObjects, o)
+		case isIstioResource(o):
+			if gw, ok := o.(*networking_v1beta1.Gateway); ok {
+				istioGateways = append(istioGateways, gw)
+			} else {
+				istioObjects = append(istioObjects, o)
+			}
+		case isGatewayAPIResource(o):
+			gatewayapiObjects = append(gatewayapiObjects, o)
+		case isOpenShiftResource(o):
+			if dc, ok := o.(*osapps_v1.DeploymentConfig); ok {
+				if _, exists := deploymentConfigs[dc.Namespace]; !exists {
+					deploymentConfigs[dc.Namespace] = []osapps_v1.DeploymentConfig{}
+				}
+				deploymentConfigs[dc.Namespace] = append(deploymentConfigs[dc.Namespace], *dc)
+			} else if proj, ok := o.(*osproject_v1.Project); ok {
+				projects = append(projects, *proj)
+			}
+		}
+	}
+
+	kubeClient := kubefake.NewSimpleClientset(kubeObjects...)
+	istioClient := istiofake.NewSimpleClientset(istioObjects...)
+	gatewayAPIClient := gatewayapifake.NewSimpleClientset(gatewayapiObjects...)
+
+	// These are created separately because the fake clientset guesses the resource name based on the Kind.
+	for _, gw := range istioGateways {
+		if _, err := istioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.TODO(), gw, metav1.CreateOptions{}); err != nil {
+			panic(err)
+		}
+	}
+
+	return &FakeK8sClient{
+		ClientInterface:   kialikube.NewClient(kubeClient, istioClient, gatewayAPIClient),
+		deploymentConfigs: deploymentConfigs,
+		projects:          projects,
+		KubeClientset:     kubeClient,
+		IstioClientset:    istioClient,
+	}
+}
+
+// FakeK8sClient is an implementation of the kiali Kubernetes client interface used for tests.
+type FakeK8sClient struct {
+	OpenShift         bool
+	GatewayAPIEnabled bool
+	kialikube.ClientInterface
+	// Keeping track of the openshift objects separately since we don't use the openshift client-go
+	// and there's no underlying fake clientset.
+	// This is a map of namespace: []objects e.g. DeploymentConfig: []DeploymentConfig.
+	deploymentConfigs map[string][]osapps_v1.DeploymentConfig
+	projects          []osproject_v1.Project
+	// Underlying kubernetes clientset.
+	KubeClientset kubernetes.Interface
+	// Underlying istio clientset.
+	IstioClientset istio.Interface
+	// Underlying gateway api clientset.
+	GatewayAPIClientset gatewayapi.Interface
+	// Token is the kiali token this client uses.
+	Token string
+}
+
+func (c *FakeK8sClient) IsOpenShift() bool  { return c.OpenShift }
+func (c *FakeK8sClient) IsGatewayAPI() bool { return c.GatewayAPIEnabled }
+func (c *FakeK8sClient) GetToken() string   { return c.Token }
+
+// The openshift resources are stubbed out because Kiali talks directly to the
+// kube api for these instead of using the openshift client-go.
+func (c *FakeK8sClient) GetProject(name string) (*osproject_v1.Project, error) {
+	for _, p := range c.projects {
+		if p.Name == name {
+			return &p, nil
+		}
+	}
+	return nil, kubeerrors.NewNotFound(osproject_v1.Resource("project"), name)
+}
+
+// The openshift resources are stubbed out because Kiali talks directly to the
+// kube api for these instead of using the openshift client-go.
+func (c *FakeK8sClient) GetProjects(labelSelector string) ([]osproject_v1.Project, error) {
+	return c.projects, nil
+}
+
+func (c *FakeK8sClient) GetDeploymentConfig(namespace string, name string) (*osapps_v1.DeploymentConfig, error) {
+	for _, dc := range c.deploymentConfigs[namespace] {
+		if dc.Name == name {
+			return &dc, nil
+		}
+	}
+	return nil, kubeerrors.NewNotFound(osapps_v1.Resource("deploymentconfig"), name)
+}
+
+func (c *FakeK8sClient) GetDeploymentConfigs(namespace string) ([]osapps_v1.DeploymentConfig, error) {
+	// No namespace specified means return all.
+	if namespace == "" {
+		var all []osapps_v1.DeploymentConfig
+		for _, dcs := range c.deploymentConfigs {
+			all = append(all, dcs...)
+		}
+		return all, nil
+	}
+
+	if dcs, ok := c.deploymentConfigs[namespace]; ok {
+		return dcs, nil
+	}
+
+	return nil, nil
+}
+
+var _ kialikube.ClientInterface = &FakeK8sClient{}
diff --git a/kubernetes/kubetest/mock.go b/kubernetes/kubetest/mock.go
index 4ca4845c5..44485597c 100644
--- a/kubernetes/kubetest/mock.go
+++ b/kubernetes/kubetest/mock.go
@@ -1,6 +1,8 @@
 package kubetest
 
 import (
+	"sync"
+
 	osapps_v1 "github.com/openshift/api/apps/v1"
 	"github.com/stretchr/testify/mock"
 	"gopkg.in/square/go-jose.v2/jwt"
@@ -21,20 +23,41 @@ import (
 //// Mock for the K8SClientFactory
 
 type K8SClientFactoryMock struct {
-	mock.Mock
-	k8s kubernetes.ClientInterface
+	lock    sync.RWMutex
+	Clients map[string]kubernetes.ClientInterface
 }
 
 // Constructor
 func NewK8SClientFactoryMock(k8s kubernetes.ClientInterface) *K8SClientFactoryMock {
 	k8sClientFactory := new(K8SClientFactoryMock)
-	k8sClientFactory.k8s = k8s
+	k8sClientFactory.Clients = map[string]kubernetes.ClientInterface{kubernetes.HomeClusterName: k8s}
 	return k8sClientFactory
 }
 
+// Testing specific methods
+func (o *K8SClientFactoryMock) SetClients(clients map[string]kubernetes.ClientInterface) {
+	o.lock.Lock()
+	defer o.lock.Unlock()
+	o.Clients = clients
+}
+
 // Business Methods
 func (o *K8SClientFactoryMock) GetClient(authInfo *api.AuthInfo) (kubernetes.ClientInterface, error) {
-	return o.k8s, nil
+	o.lock.RLock()
+	defer o.lock.RUnlock()
+	return o.Clients[kubernetes.HomeClusterName], nil
+}
+
+func (o *K8SClientFactoryMock) GetSAClients() map[string]kubernetes.ClientInterface {
+	o.lock.RLock()
+	defer o.lock.RUnlock()
+	return o.Clients
+}
+
+func (o *K8SClientFactoryMock) GetSAHomeClusterClient() kubernetes.ClientInterface {
+	o.lock.RLock()
+	defer o.lock.RUnlock()
+	return o.Clients[kubernetes.HomeClusterName]
 }
 
 /////
@@ -139,20 +162,20 @@ func (o *K8SClientMock) GetTokenSubject(authInfo *api.AuthInfo) (string, error)
 }
 
 func (o *K8SClientMock) MockService(namespace, name string) {
-	s := fakeService(namespace, name)
+	s := FakeService(namespace, name)
 	o.On("GetService", namespace, name).Return(&s, nil)
 }
 
 func (o *K8SClientMock) MockServices(namespace string, names []string) {
 	services := []core_v1.Service{}
 	for _, name := range names {
-		services = append(services, fakeService(namespace, name))
+		services = append(services, FakeService(namespace, name))
 	}
 	o.On("GetServices", namespace, mock.AnythingOfType("map[string]string")).Return(services, nil)
 	o.On("GetDeployments", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return([]apps_v1.Deployment{}, nil)
 }
 
-func fakeService(namespace, name string) core_v1.Service {
+func FakeService(namespace, name string) core_v1.Service {
 	return core_v1.Service{
 		ObjectMeta: meta_v1.ObjectMeta{
 			Name:      name,
diff --git a/kubernetes/kubetest/mock_kubernetes.go b/kubernetes/kubetest/mock_kubernetes.go
index 7b2e79ae8..f42354671 100644
--- a/kubernetes/kubetest/mock_kubernetes.go
+++ b/kubernetes/kubetest/mock_kubernetes.go
@@ -8,8 +8,13 @@ import (
 	auth_v1 "k8s.io/api/authorization/v1"
 	batch_v1 "k8s.io/api/batch/v1"
 	core_v1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/kubernetes"
 )
 
+func (o *K8SClientMock) Kube() kubernetes.Interface {
+	return nil
+}
+
 func (o *K8SClientMock) GetClusterServicesByLabels(labelsSelector string) ([]core_v1.Service, error) {
 	args := o.Called(labelsSelector)
 	return args.Get(0).([]core_v1.Service), args.Error(1)
diff --git a/status/versions_test.go b/status/versions_test.go
index e6bd40778..37b00194a 100644
--- a/status/versions_test.go
+++ b/status/versions_test.go
@@ -155,459 +155,3 @@ func TestParseIstioRawVersion(t *testing.T) {
 		}
 	}
 }
-
-func TestValidateVersion(t *testing.T) {
-	result := validateVersion(">= 0.7.1", "0.7.1")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 0.7.1 is >= 0.7.1")
-	}
-
-	result = validateVersion(">= 0.7.1", "0.8.1")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 0.8.1 is >= 0.7.1")
-	}
-
-	result = validateVersion(">= 0.7.1", "1.3.0")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 1.3.0 is >= 0.7.1")
-	}
-
-	result = validateVersion("== 0.7.1", "1.3.0")
-
-	if result {
-		t.Errorf("validateVersion was incorrect, got true, want false, 1.3.0 is not == 0.7.1")
-	}
-
-	result = validateVersion("> 0.7.1", "1.3.0")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 1.3.0 is > 0.7.1")
-	}
-
-	result = validateVersion(">= 0.7.1", "0.6.3")
-
-	if result {
-		t.Errorf("validateVersion was incorrect, got true, want false, 0.6.3 is not >= 0.7.1")
-	}
-
-	result = validateVersion("> 0.7.1", "0.6.3")
-
-	if result {
-		t.Errorf("validateVersion was incorrect, got true, want false, 0.6.3 is not > 0.7.1")
-	}
-
-	// Alpha / Beta versions
-
-	result = validateVersion("> 0.7.1", "0.8.3-alpha")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 0.8.3-alpha is > 0.7.1")
-	}
-
-	result = validateVersion(">= 0.8.3-alpha", "0.8.3-alpha")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 0.8.3-alpha is >= 0.8.3-alpha")
-	}
-
-	result = validateVersion("> 1.5.0-alpha", "1.6.0-beta.0")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 1.5.0-alpha is <= 1.6.0-beta.0")
-	}
-
-	// Longer releases
-
-	result = validateVersion(">= 0.8.3", "0.8.3.1")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 0.8.3.1 is >= 0.8.3")
-	}
-
-	result = validateVersion("> 0.9", "0.9.1.1")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 0.9.1.1 is > 0.9")
-	}
-
-	result = validateVersion("> 0.8.1.1", "0.8.2")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 0.8.2 is > 0.8.1.1")
-	}
-
-	result = validateVersion("> 0.8.1.1", "1")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 1 is > 0.8.1.1")
-	}
-
-	result = validateVersion("< 1.5", "1.4")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 1.4 is < 1.5")
-	}
-
-	result = validateVersion("< 1.5", "1.4.2")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 1.4.2 is < 1.5")
-	}
-
-	result = validateVersion(">= 1.5", "1.5")
-
-	if !result {
-		t.Errorf("validateVersion was incorrect, got false, want true, 1.5 is >= 1.5")
-	}
-
-	result = validateVersion("< 1.5", "1.5.2")
-
-	if result {
-		t.Errorf("validateVersion was incorrect, got true, want false, 1.5.2 is > 1.5")
-	}
-
-}
-
-// TestMeshVersionCompatible check meshVersion compatibility witk Kiali
-func TestMeshVersionCompatible(t *testing.T) {
-
-	// versionsToTestStruct struct for version compatibility test cases
-	type versionsToTestStruct struct {
-		meshVersion string
-		name        string
-		version     string
-		supported   bool
-	}
-
-	versionsToTest := []versionsToTestStruct{
-		{
-			name:        "Istio",
-			version:     "1.59.1",
-			meshVersion: "1.16",
-			supported:   true,
-		},
-		{
-			name:        "Istio",
-			version:     "1.59.0",
-			meshVersion: "1.16",
-			supported:   false,
-		},
-		{
-			name:        "Istio",
-			version:     "1.59.0",
-			meshVersion: "1.15",
-			supported:   true,
-		},
-		{
-			name:        "Istio",
-			version:     "1.55.0",
-			meshVersion: "1.15",
-			supported:   true,
-		},
-		{
-			name:        "Istio",
-			version:     "1.54.0",
-			meshVersion: "1.15",
-			supported:   false,
-		},
-		{
-			name:        "Istio",
-			version:     "1.45.1",
-			meshVersion: "1.14",
-			supported:   false,
-		},
-		{
-			name:        "Istio",
-			version:     "1.50.0",
-			meshVersion: "1.14",
-			supported:   true,
-		},
-		{
-			name:        "Istio",
-			version:     "1.45.1",
-			meshVersion: "1.13",
-			supported:   true,
-		},
-		{
-			name:        "Istio",
-			version:     "1.44.0",
-			meshVersion: "1.13",
-			supported:   false,
-		},
-		{
-			name:        "Istio",
-			version:     "1.44.0",
-			meshVersion: "1.12",
-			supported:   true,
-		},
-		{
-			name:        "Istio",
-			version:     "1.44.1",
-			meshVersion: "1.12",
-			supported:   false,
-		},
-		{
-			name:        "Istio",
-			version:     "1.43.0",
-			meshVersion: "1.12",
-			supported:   true,
-		},
-		{
-			name:        "Istio",
-			version:     "1.41.0",
-			meshVersion: "1.12.1",
-			supported:   false,
-		},
-		{
-			name:        "Istio",
-			version:     "1.38.2",
-			meshVersion: "1.11.2",
-			supported:   true,
-		},
-		{
-			name:        "Istio Snapshot",
-			version:     "1.38.0",
-			meshVersion: "1.11",
-			supported:   false,
-		},
-		{
-			name:        "Istio Snapshot",
-			version:     "1.34.2",
-			meshVersion: "1.10",
-			supported:   true,
-		},
-		{
-			name:        "Istio Snapshot",
-			version:     "1.38.1",
-			meshVersion: "1.10",
-			supported:   false,
-		},
-		{
-			name:        "Istio Snapshot",
-			version:     "1.29.2",
-			meshVersion: "1.9",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.36",
-			meshVersion: "1.9",
-			supported:   false,
-		},
-		{
-			name:        "Istio RC",
-			version:     "1.29.1",
-			meshVersion: "1.8",
-			supported:   false,
-		},
-		{
-			name:        "Istio RC",
-			version:     "1.26.2",
-			meshVersion: "1.8.1",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.22.2",
-			meshVersion: "1.7",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.22.2",
-			meshVersion: "1.7.3",
-			supported:   true,
-		},
-		{
-			name:        "Istio RC",
-			version:     "1.18.2",
-			meshVersion: "1.6",
-			supported:   true,
-		},
-		{
-			name:        "Istio RC",
-			version:     "1.18.2",
-			meshVersion: "1.6.1",
-			supported:   true,
-		},
-		{
-			name:        "Istio RC",
-			version:     "1.22",
-			meshVersion: "1.6",
-			supported:   false,
-		},
-		{
-			name:        "Istio RC",
-			version:     "1.22",
-			meshVersion: "1.5",
-			supported:   false,
-		},
-		{
-			name:        "Istio RC",
-			version:     "1.22",
-			meshVersion: "1.5",
-			supported:   false,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.22",
-			meshVersion: "1.5.1",
-			supported:   false,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.17",
-			meshVersion: "1.0",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.19",
-			meshVersion: "1.0",
-			supported:   false,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.17",
-			meshVersion: "1.1",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.19",
-			meshVersion: "1.1",
-			supported:   false,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.17",
-			meshVersion: "1.2",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.19",
-			meshVersion: "1.2",
-			supported:   false,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.17",
-			meshVersion: "1.3",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.19",
-			meshVersion: "1.3",
-			supported:   false,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.17",
-			meshVersion: "1.4",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.19",
-			meshVersion: "1.4",
-			supported:   false,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.17",
-			meshVersion: "1.5",
-			supported:   true,
-		},
-		{
-			name:        "Istio Dev",
-			version:     "1.19",
-			meshVersion: "1.5",
-			supported:   false,
-		},
-		{
-			name:        "istioProductNameUnknown",
-			version:     "1.18.2",
-			meshVersion: "1.6.1",
-			supported:   false,
-		},
-		{
-			name:        "Maistra",
-			version:     "1.24",
-			meshVersion: "2.0",
-			supported:   false,
-		},
-		{
-			name:        "MaistrX",
-			version:     "1.24",
-			meshVersion: "2.0",
-			supported:   false,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.48",
-			meshVersion: "2.2",
-			supported:   true,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.36",
-			meshVersion: "2.1",
-			supported:   true,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.24",
-			meshVersion: "2.0",
-			supported:   true,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.12",
-			meshVersion: "1.1",
-			supported:   true,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.48.99-SNAPSHOT",
-			meshVersion: "2.2.777.RC",
-			supported:   true,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.99",
-			meshVersion: "1.99",
-			supported:   false,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.36",
-			meshVersion: "2.2",
-			supported:   false,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.24",
-			meshVersion: "2.2",
-			supported:   false,
-		},
-		{
-			name:        "OpenShift Service Mesh",
-			version:     "1.12",
-			meshVersion: "2.2",
-			supported:   false,
-		},
-	}
-
-	for _, versionToTest := range versionsToTest {
-		p := CheckMeshVersion(versionToTest.name, versionToTest.meshVersion, versionToTest.version)
-		t.Logf("Checking:\n[%+v]\n", versionToTest)
-		if p != versionToTest.supported {
-			t.Errorf("Cannot validate [%+v] - version range is incorrect: %+v", versionToTest, p)
-		}
-	}
-}
diff --git a/tests/data/k8sgateway_data.go b/tests/data/k8sgateway_data.go
new file mode 100644
index 000000000..5696ae665
--- /dev/null
+++ b/tests/data/k8sgateway_data.go
@@ -0,0 +1,102 @@
+package data
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	k8s_networking_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+	"github.com/kiali/kiali/kubernetes"
+)
+
+func CreateEmptyHTTPRoute(name string, namespace string, hosts []string) *k8s_networking_v1beta1.HTTPRoute {
+	vs := k8s_networking_v1beta1.HTTPRoute{}
+	vs.Name = name
+	vs.Namespace = namespace
+	for _, host := range hosts {
+		vs.Spec.Hostnames = append(vs.Spec.Hostnames, k8s_networking_v1beta1.Hostname(host))
+	}
+	return &vs
+}
+
+func CreateHTTPRoute(name string, namespace string, gateway string, hosts []string) *k8s_networking_v1beta1.HTTPRoute {
+	return AddParentRefToHTTPRoute(gateway, namespace, CreateEmptyHTTPRoute(name, namespace, hosts))
+}
+
+func AddParentRefToHTTPRoute(name, namespace string, rt *k8s_networking_v1beta1.HTTPRoute) *k8s_networking_v1beta1.HTTPRoute {
+	ns := k8s_networking_v1beta1.Namespace(namespace)
+	group := k8s_networking_v1beta1.Group(kubernetes.K8sNetworkingGroupVersionV1Beta1.Group)
+	kind := k8s_networking_v1beta1.Kind(kubernetes.K8sActualGatewayType)
+	rt.Spec.ParentRefs = append(rt.Spec.ParentRefs, k8s_networking_v1beta1.ParentReference{
+		Name:      k8s_networking_v1beta1.ObjectName(name),
+		Namespace: &ns,
+		Group:     &group,
+		Kind:      &kind})
+	return rt
+}
+
+func AddBackendRefToHTTPRoute(name, namespace string, rt *k8s_networking_v1beta1.HTTPRoute) *k8s_networking_v1beta1.HTTPRoute {
+	kind := k8s_networking_v1beta1.Kind("Service")
+	var ns k8s_networking_v1beta1.Namespace
+	if namespace != "" {
+		ns = k8s_networking_v1beta1.Namespace(namespace)
+	}
+	backendRef := k8s_networking_v1beta1.HTTPBackendRef{
+		BackendRef: k8s_networking_v1beta1.BackendRef{
+			BackendObjectReference: k8s_networking_v1beta1.BackendObjectReference{
+				Kind:      &kind,
+				Name:      k8s_networking_v1beta1.ObjectName(name),
+				Namespace: &ns,
+			},
+		},
+	}
+	rule := k8s_networking_v1beta1.HTTPRouteRule{}
+	rule.BackendRefs = append(rule.BackendRefs, backendRef)
+	rt.Spec.Rules = append(rt.Spec.Rules, rule)
+	return rt
+}
+
+func CreateEmptyK8sGateway(name, namespace string) *k8s_networking_v1beta1.Gateway {
+	gw := k8s_networking_v1beta1.Gateway{}
+	gw.Name = name
+	gw.Namespace = namespace
+
+	gw.Kind = kubernetes.K8sActualGatewayType
+	gw.APIVersion = kubernetes.K8sApiNetworkingVersionV1Beta1
+	gw.Spec.GatewayClassName = "istio"
+	return &gw
+}
+
+func AddListenerToK8sGateway(listener k8s_networking_v1beta1.Listener, gw *k8s_networking_v1beta1.Gateway) *k8s_networking_v1beta1.Gateway {
+	gw.Spec.Listeners = append(gw.Spec.Listeners, listener)
+	return gw
+}
+
+func AddGwAddressToK8sGateway(address k8s_networking_v1beta1.GatewayAddress, gw *k8s_networking_v1beta1.Gateway) *k8s_networking_v1beta1.Gateway {
+	gw.Spec.Addresses = append(gw.Spec.Addresses, address)
+	return gw
+}
+
+func CreateListener(name string, hostname string, port int, protocol string) k8s_networking_v1beta1.Listener {
+	hn := k8s_networking_v1beta1.Hostname(hostname)
+	listener := k8s_networking_v1beta1.Listener{
+		Name:     k8s_networking_v1beta1.SectionName(name),
+		Hostname: &hn,
+		Port:     k8s_networking_v1beta1.PortNumber(port),
+		Protocol: k8s_networking_v1beta1.ProtocolType(protocol),
+	}
+	return listener
+}
+
+func CreateGWAddress(addrType k8s_networking_v1beta1.AddressType, value string) k8s_networking_v1beta1.GatewayAddress {
+	address := k8s_networking_v1beta1.GatewayAddress{
+		Type:  &addrType,
+		Value: value,
+	}
+	return address
+}
+
+func UpdateConditionWithError(k8sgw *k8s_networking_v1beta1.Gateway) *k8s_networking_v1beta1.Gateway {
+	condition := metav1.Condition{Type: "Ready", Status: "False", Reason: "", Message: "Fake msg"}
+	k8sgw.Status.Conditions = append(k8sgw.Status.Conditions, condition)
+
+	return k8sgw
+}
diff --git a/tests/integration/assets/bookinfo-k8sgateways-addresses.yaml b/tests/integration/assets/bookinfo-k8sgateways-addresses.yaml
new file mode 100644
index 000000000..04db42a50
--- /dev/null
+++ b/tests/integration/assets/bookinfo-k8sgateways-addresses.yaml
@@ -0,0 +1,41 @@
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: gatewayapi
+  namespace: bookinfo
+  labels:
+    app: gatewayapi
+spec:
+  gatewayClassName: istio
+  listeners:
+    - name: default
+      hostname: "bookinfo-istio-system"
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: All
+  addresses:
+    - type: IPAddress
+      value: "192.168.1.1"
+---
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: gatewayapi2
+  namespace: bookinfo
+  labels:
+    app: gatewayapi
+spec:
+  gatewayClassName: istio
+  listeners:
+    - name: default
+      hostname: "bookinfo-istio-system2"
+      port: 8080
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: All
+  addresses:
+    - type: IPAddress
+      value: "192.168.1.1"
diff --git a/tests/integration/assets/bookinfo-k8sgateways-listeners.yaml b/tests/integration/assets/bookinfo-k8sgateways-listeners.yaml
new file mode 100644
index 000000000..5ada0e4c4
--- /dev/null
+++ b/tests/integration/assets/bookinfo-k8sgateways-listeners.yaml
@@ -0,0 +1,35 @@
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: gatewayapi
+  namespace: bookinfo
+  labels:
+    app: gatewayapi
+spec:
+  gatewayClassName: istio
+  listeners:
+    - name: default
+      hostname: "bookinfo-istio-system"
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: All
+---
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: gatewayapi2
+  namespace: bookinfo
+  labels:
+    app: gatewayapi
+spec:
+  gatewayClassName: istio
+  listeners:
+    - name: default
+      hostname: "bookinfo-istio-system"
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: All
diff --git a/tests/integration/assets/bookinfo-k8sgateways.yaml b/tests/integration/assets/bookinfo-k8sgateways.yaml
new file mode 100644
index 000000000..69b066abc
--- /dev/null
+++ b/tests/integration/assets/bookinfo-k8sgateways.yaml
@@ -0,0 +1,39 @@
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: gatewayapi
+  namespace: bookinfo
+  labels:
+    app: gatewayapi
+spec:
+  gatewayClassName: istio
+  listeners:
+    - name: default
+      hostname: "bookinfo-istio-system"
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: All
+  addresses:
+    - type: Hostname
+      value: "google.com"
+---
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: HTTPRoute
+metadata:
+  name: httproute
+  namespace: bookinfo
+spec:
+  parentRefs:
+    - name: gatewayapi
+      namespace: bookinfo
+  hostnames: ["bookinfo-istio-system"]
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /get
+      backendRefs:
+        - name: httpbin
+          port: 8000
diff --git a/tests/integration/assets/bookinfo-k8shttproutes-gateways.yaml b/tests/integration/assets/bookinfo-k8shttproutes-gateways.yaml
new file mode 100644
index 000000000..6236c1daf
--- /dev/null
+++ b/tests/integration/assets/bookinfo-k8shttproutes-gateways.yaml
@@ -0,0 +1,18 @@
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: HTTPRoute
+metadata:
+  name: httproute
+  namespace: bookinfo
+spec:
+  parentRefs:
+    - name: gatewayapiwrong
+      namespace: bookinfo
+  hostnames: ["bookinfo-istio-system"]
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /get
+      backendRefs:
+        - name: httpbin
+          port: 8000
diff --git a/tests/integration/assets/bookinfo-k8shttproutes-services.yaml b/tests/integration/assets/bookinfo-k8shttproutes-services.yaml
new file mode 100644
index 000000000..80bf9164d
--- /dev/null
+++ b/tests/integration/assets/bookinfo-k8shttproutes-services.yaml
@@ -0,0 +1,39 @@
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: gatewayapiservices
+  namespace: bookinfo
+  labels:
+    app: gatewayapi
+spec:
+  gatewayClassName: istio
+  listeners:
+    - name: default
+      hostname: "bookinfo-istio-system"
+      port: 80
+      protocol: HTTP
+      allowedRoutes:
+        namespaces:
+          from: All
+  addresses:
+    - type: Hostname
+      value: "google.com"
+---
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: HTTPRoute
+metadata:
+  name: httprouteservices
+  namespace: bookinfo
+spec:
+  parentRefs:
+    - name: gatewayapiservices
+      namespace: bookinfo
+  hostnames: ["bookinfo-istio-system"]
+  rules:
+    - matches:
+        - path:
+            type: PathPrefix
+            value: /get
+      backendRefs:
+        - name: wrong
+          port: 8000
diff --git a/tests/integration/assets/bookinfo-service-entry-external.yaml b/tests/integration/assets/bookinfo-service-entry-external.yaml
new file mode 100644
index 000000000..3e14cfccf
--- /dev/null
+++ b/tests/integration/assets/bookinfo-service-entry-external.yaml
@@ -0,0 +1,16 @@
+apiVersion: networking.istio.io/v1alpha3
+kind: ServiceEntry
+metadata:
+  name: external-svc-https
+  namespace: bookinfo
+spec:
+  hosts:
+    - api.dropboxapi.com
+    - www.googleapis.com
+    - api.facebook.com
+  location: MESH_EXTERNAL
+  ports:
+    - number: 443
+      name: https
+      protocol: TLS
+  resolution: DNS
\ No newline at end of file
diff --git a/tests/integration/tests/config_validations_test.go b/tests/integration/tests/config_validations_test.go
index 2939dabd2..8b587d083 100644
--- a/tests/integration/tests/config_validations_test.go
+++ b/tests/integration/tests/config_validations_test.go
@@ -22,7 +22,7 @@ func TestAuthPolicyPrincipalsError(t *testing.T) {
 	defer utils.DeleteFile(filePath, utils.BOOKINFO)
 	assert.True(utils.ApplyFile(filePath, utils.BOOKINFO))
 
-	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.AuthorizationPolicies, assert)
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.AuthorizationPolicies, false, assert)
 
 	assert.Nil(err)
 	assert.NotNil(config)
@@ -51,7 +51,7 @@ func TestServiceEntryLabels(t *testing.T) {
 
 	// the DR with matching labels with SE
 	name := "dest-rule-labels"
-	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.DestinationRules, assert)
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.DestinationRules, false, assert)
 	assert.Nil(err)
 	assert.NotNil(config)
 	assert.True(config.IstioValidation.Valid)
@@ -66,7 +66,7 @@ func TestServiceEntryLabelsNotMatch(t *testing.T) {
 
 	// the DR with error, labels not match with SE
 	name := "dest-rule-labels-wrong"
-	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.DestinationRules, assert)
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.DestinationRules, false, assert)
 	assert.Nil(err)
 	assert.NotNil(config)
 	assert.False(config.IstioValidation.Valid)
@@ -75,15 +75,117 @@ func TestServiceEntryLabelsNotMatch(t *testing.T) {
 	assert.Equal("This subset's labels are not found in any matching host", config.IstioValidation.Checks[0].Message)
 }
 
-func getConfigDetails(namespace, name, configType string, assert *assert.Assertions) (*models.IstioConfigDetails, error) {
+func TestK8sGatewaysAddressesError(t *testing.T) {
+	name := "gatewayapi"
+	assert := assert.New(t)
+	filePath := path.Join(cmd.KialiProjectRoot, utils.ASSETS+"/bookinfo-k8sgateways-addresses.yaml")
+	defer utils.DeleteFile(filePath, utils.BOOKINFO)
+	assert.True(utils.ApplyFile(filePath, utils.BOOKINFO))
+
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.K8sGateways, true, assert)
+
+	assert.Nil(err)
+	assert.NotNil(config)
+	assert.Equal(kubernetes.K8sGateways, config.ObjectType)
+	assert.Equal(utils.BOOKINFO, config.Namespace.Name)
+	assert.NotNil(config.K8sGateway)
+	assert.Equal(name, config.K8sGateway.Name)
+	assert.Equal(utils.BOOKINFO, config.K8sGateway.Namespace)
+	assert.NotNil(config.IstioValidation)
+	assert.Equal(name, config.IstioValidation.Name)
+	assert.Equal("k8sgateway", config.IstioValidation.ObjectType)
+	assert.NotEmpty(config.IstioValidation.Checks)
+	assert.Equal(models.WarningSeverity, config.IstioValidation.Checks[0].Severity)
+	assert.Equal("More than one K8s Gateway for the same address and type combination", config.IstioValidation.Checks[0].Message)
+}
+
+func TestK8sGatewaysListenersError(t *testing.T) {
+	name := "gatewayapi"
+	assert := assert.New(t)
+	filePath := path.Join(cmd.KialiProjectRoot, utils.ASSETS+"/bookinfo-k8sgateways-listeners.yaml")
+	defer utils.DeleteFile(filePath, utils.BOOKINFO)
+	assert.True(utils.ApplyFile(filePath, utils.BOOKINFO))
+
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.K8sGateways, true, assert)
+
+	assert.Nil(err)
+	assert.NotNil(config)
+	assert.Equal(kubernetes.K8sGateways, config.ObjectType)
+	assert.Equal(utils.BOOKINFO, config.Namespace.Name)
+	assert.NotNil(config.K8sGateway)
+	assert.Equal(name, config.K8sGateway.Name)
+	assert.Equal(utils.BOOKINFO, config.K8sGateway.Namespace)
+	assert.NotNil(config.IstioValidation)
+	assert.Equal(name, config.IstioValidation.Name)
+	assert.Equal("k8sgateway", config.IstioValidation.ObjectType)
+	assert.NotEmpty(config.IstioValidation.Checks)
+	assert.Equal(models.WarningSeverity, config.IstioValidation.Checks[0].Severity)
+	assert.Equal("More than one K8s Gateway for the same host port combination", config.IstioValidation.Checks[0].Message)
+}
+
+func TestK8sHTTPRoutesGatewaysError(t *testing.T) {
+	name := "httproute"
+	assert := assert.New(t)
+	filePath := path.Join(cmd.KialiProjectRoot, utils.ASSETS+"/bookinfo-k8shttproutes-gateways.yaml")
+	defer utils.DeleteFile(filePath, utils.BOOKINFO)
+	assert.True(utils.ApplyFile(filePath, utils.BOOKINFO))
+
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.K8sHTTPRoutes, true, assert)
+
+	assert.Nil(err)
+	assert.NotNil(config)
+	assert.Equal(kubernetes.K8sHTTPRoutes, config.ObjectType)
+	assert.Equal(utils.BOOKINFO, config.Namespace.Name)
+	assert.NotNil(config.K8sHTTPRoute)
+	assert.Equal(name, config.K8sHTTPRoute.Name)
+	assert.Equal(utils.BOOKINFO, config.K8sHTTPRoute.Namespace)
+	assert.NotNil(config.IstioValidation)
+	assert.False(config.IstioValidation.Valid)
+	assert.Equal(name, config.IstioValidation.Name)
+	assert.Equal("k8shttproute", config.IstioValidation.ObjectType)
+	assert.NotEmpty(config.IstioValidation.Checks)
+	assert.Equal(models.ErrorSeverity, config.IstioValidation.Checks[0].Severity)
+	assert.Equal("HTTPRoute is pointing to a non-existent K8s gateway", config.IstioValidation.Checks[0].Message)
+}
+
+func TestK8sHTTPRoutesServicesError(t *testing.T) {
+	name := "httprouteservices"
+	assert := assert.New(t)
+	filePath := path.Join(cmd.KialiProjectRoot, utils.ASSETS+"/bookinfo-k8shttproutes-services.yaml")
+	defer utils.DeleteFile(filePath, utils.BOOKINFO)
+	assert.True(utils.ApplyFile(filePath, utils.BOOKINFO))
+
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.K8sHTTPRoutes, true, assert)
+
+	assert.Nil(err)
+	assert.NotNil(config)
+	assert.Equal(kubernetes.K8sHTTPRoutes, config.ObjectType)
+	assert.Equal(utils.BOOKINFO, config.Namespace.Name)
+	assert.NotNil(config.K8sHTTPRoute)
+	assert.Equal(name, config.K8sHTTPRoute.Name)
+	assert.Equal(utils.BOOKINFO, config.K8sHTTPRoute.Namespace)
+	assert.NotNil(config.IstioValidation)
+	assert.False(config.IstioValidation.Valid)
+	assert.Equal(name, config.IstioValidation.Name)
+	assert.Equal("k8shttproute", config.IstioValidation.ObjectType)
+	assert.NotEmpty(config.IstioValidation.Checks)
+	assert.Equal(models.ErrorSeverity, config.IstioValidation.Checks[0].Severity)
+	assert.Equal("BackendRef on rule doesn't have a valid service (host not found)", config.IstioValidation.Checks[0].Message)
+}
+
+func getConfigDetails(namespace, name, configType string, skipReferences bool, assert *assert.Assertions) (*models.IstioConfigDetails, error) {
 	config, _, err := utils.IstioConfigDetails(namespace, name, configType)
 	if err == nil && config != nil && config.IstioValidation != nil && config.IstioReferences != nil {
 		return config, nil
 	}
-	pollErr := wait.Poll(time.Second, time.Minute, func() (bool, error) {
+	pollErr := wait.Poll(time.Second, time.Minute*5, func() (bool, error) {
 		config, _, err = utils.IstioConfigDetails(namespace, name, configType)
-		if err == nil && config != nil && config.IstioValidation != nil && config.IstioReferences != nil {
-			return true, nil
+		if err == nil && config != nil && config.IstioValidation != nil {
+			if !skipReferences && config.IstioReferences != nil {
+				return true, nil
+			} else if skipReferences {
+				return true, nil
+			}
 		}
 		return false, nil
 	})
diff --git a/tests/integration/tests/istio_config_test.go b/tests/integration/tests/istio_config_test.go
index 302b1572a..8a3bd92d1 100644
--- a/tests/integration/tests/istio_config_test.go
+++ b/tests/integration/tests/istio_config_test.go
@@ -1,16 +1,22 @@
 package tests
 
 import (
+	"path"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/tests/integration/utils"
+	"github.com/kiali/kiali/tools/cmd"
 )
 
 func TestIstioConfigList(t *testing.T) {
 	assert := assert.New(t)
+	filePath := path.Join(cmd.KialiProjectRoot, utils.ASSETS+"/bookinfo-k8sgateways.yaml")
+	defer utils.DeleteFile(filePath, utils.BOOKINFO)
+	assert.True(utils.ApplyFile(filePath, utils.BOOKINFO))
+
 	configList, err := utils.IstioConfigsList(utils.BOOKINFO)
 
 	assert.Nil(err)
@@ -19,6 +25,9 @@ func TestIstioConfigList(t *testing.T) {
 
 func TestIstioConfigs(t *testing.T) {
 	assert := assert.New(t)
+	filePath := path.Join(cmd.KialiProjectRoot, utils.ASSETS+"/bookinfo-k8sgateways.yaml")
+	defer utils.DeleteFile(filePath, utils.BOOKINFO)
+	assert.True(utils.ApplyFile(filePath, utils.BOOKINFO))
 	configMap, err := utils.IstioConfigs()
 
 	assert.Nil(err)
@@ -66,6 +75,16 @@ func assertConfigs(configList utils.IstioConfigListJson, assert *assert.Assertio
 		assert.True(gw.Namespace == configList.Namespace.Name)
 		assert.NotNil(gw.Name)
 	}
+	assert.NotNil(configList.K8sGateways)
+	for _, gw := range configList.K8sGateways {
+		assert.True(gw.Namespace == configList.Namespace.Name)
+		assert.NotNil(gw.Name)
+	}
+	assert.NotNil(configList.K8sHTTPRoutes)
+	for _, gw := range configList.K8sHTTPRoutes {
+		assert.True(gw.Namespace == configList.Namespace.Name)
+		assert.NotNil(gw.Name)
+	}
 	assert.NotNil(configList.RequestAuthentications)
 	for _, ra := range configList.RequestAuthentications {
 		assert.True(ra.Namespace == configList.Namespace.Name)
diff --git a/tests/integration/tests/no_istiod_test.go b/tests/integration/tests/no_istiod_test.go
new file mode 100644
index 000000000..eac198f9a
--- /dev/null
+++ b/tests/integration/tests/no_istiod_test.go
@@ -0,0 +1,150 @@
+package tests
+
+import (
+	"os/exec"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/log"
+	"github.com/kiali/kiali/tests/integration/utils"
+)
+
+var ocCommand = utils.NewExecCommand()
+
+func update_istio_api_enabled(value bool) {
+	original := !value
+
+	cmdGetProp := ocCommand + " get cm kiali -n istio-system -o yaml | grep 'istio_api_enabled'"
+	getPropOutput, _ := exec.Command("bash", "-c", cmdGetProp).Output()
+
+	if len(string(getPropOutput)) == 0 {
+		// Is the property is not there, we should add it, instead of replacing
+		cmdReplacecm3 := ocCommand + " get cm kiali -n istio-system -o yaml | sed -e 's|root_namespace: istio-system|root_namespace: istio-system'\r'        istio_api_enabled: " + strconv.FormatBool(value) + "|' | " + ocCommand + " apply -f -"
+		_, err := exec.Command("bash", "-c", cmdReplacecm3).Output()
+		if err != nil {
+			log.Errorf("Error updating config map: %s", err.Error())
+		}
+
+	} else {
+		cmdReplacecm := ocCommand + " get cm kiali -n istio-system -o yaml | sed -e 's|istio_api_enabled: " + strconv.FormatBool(original) + "|istio_api_enabled: " + strconv.FormatBool(value) + "|' | " + ocCommand + " apply -f -"
+		_, err := exec.Command("bash", "-c", cmdReplacecm).Output()
+		if err != nil {
+			log.Errorf("Error updating config map: %s", err.Error())
+		}
+	}
+
+	// Restart kiali pod
+	// Get kiali pod name
+	cmdGetPodName := ocCommand + " get pods -o name -n istio-system | egrep kiali | sed 's|pod/||'"
+	kialiPodName, err2 := exec.Command("bash", "-c", cmdGetPodName).Output()
+	podName := strings.Replace(string(kialiPodName), "\n", "", -1)
+	log.Debugf("Kiali pod name: %s", podName)
+
+	if err2 == nil {
+		// Restart
+		cmd3 := ocCommand + " delete pod " + podName + " -n istio-system"
+		_, err3 := exec.Command("bash", "-c", cmd3).Output()
+		log.Debugf("Delete pod command: %s", cmd3)
+
+		if err3 == nil {
+			waitCmd := ocCommand + " wait --for=condition=ready pod -l app=kiali -n istio-system"
+			out, err4 := exec.Command("bash", "-c", waitCmd).Output()
+
+			log.Debugf("Waiting for condition to met %s", out)
+
+			if err4 != nil {
+				log.Errorf("Error waiting for pod %s ", err4.Error())
+			}
+		}
+	}
+}
+
+func TestNoIstiod(t *testing.T) {
+	defer update_istio_api_enabled(true)
+	update_istio_api_enabled(false)
+	t.Run("ServicesListNoRegistryServices", servicesListNoRegistryServices)
+	t.Run("NoProxyStatus", noProxyStatus)
+	t.Run("istioStatus", istioStatus)
+	t.Run("emptyValidations", emptyValidations)
+}
+
+func servicesListNoRegistryServices(t *testing.T) {
+	assert := assert.New(t)
+	serviceList, err := utils.ServicesList(utils.BOOKINFO)
+
+	assert.Nil(err)
+	assert.NotEmpty(serviceList)
+	assert.True(len(serviceList.Services) >= 4)
+	sl := len(serviceList.Services)
+
+	// Deploy an external service entry
+	applySe := ocCommand + " apply -f ../assets/bookinfo-service-entry-external.yaml"
+	_, err2 := exec.Command("bash", "-c", applySe).Output()
+	if err2 != nil {
+		log.Errorf("Failed to execute command: %s", applySe)
+	}
+
+	// The service result should be the same
+	serviceList2, err3 := utils.ServicesList(utils.BOOKINFO)
+	assert.True(len(serviceList2.Services) == sl)
+	if err3 != nil {
+		log.Errorf("Failed to execute command: %s", applySe)
+	}
+
+	// Now, create a Service Entry (Part of th
+	assert.NotNil(serviceList.Validations)
+	assert.Equal(utils.BOOKINFO, serviceList.Namespace.Name)
+
+	// Cleanup
+	rmSe := ocCommand + " delete -f ../assets/bookinfo-service-entry-external.yaml"
+	_, err4 := exec.Command("bash", "-c", rmSe).Output()
+	if err4 != nil {
+		log.Errorf("Failed to execute command: %s", rmSe)
+	}
+}
+
+func noProxyStatus(t *testing.T) {
+	name := "details-v1"
+	assert := assert.New(t)
+	wl, _, err := utils.WorkloadDetails(name, utils.BOOKINFO)
+
+	assert.Nil(err)
+	assert.NotNil(wl)
+	assert.Equal(name, wl.Name)
+	assert.Equal("Deployment", wl.Type)
+	assert.NotNil(wl.Pods)
+	for _, pod := range wl.Pods {
+		assert.NotEmpty(pod.Status)
+		assert.NotEmpty(pod.Name)
+		assert.Empty(pod.ProxyStatus)
+	}
+}
+
+func emptyValidations(t *testing.T) {
+	name := "bookinfo-gateway"
+	assert := assert.New(t)
+
+	config, err := getConfigDetails(utils.BOOKINFO, name, kubernetes.Gateways, true, assert)
+
+	assert.Nil(err)
+	assert.NotNil(config)
+	assert.Equal(kubernetes.Gateways, config.ObjectType)
+	assert.Equal(utils.BOOKINFO, config.Namespace.Name)
+	assert.NotNil(config.Gateway)
+	assert.Equal(name, config.Gateway.Name)
+	assert.Equal(utils.BOOKINFO, config.Gateway.Namespace)
+	assert.Equal(len(config.IstioValidation.Checks), 0)
+	assert.Equal(len(config.IstioValidation.References), 0)
+}
+
+func istioStatus(t *testing.T) {
+	assert := assert.New(t)
+
+	isEnabled, err := utils.IstioApiEnabled()
+	assert.Nil(err)
+	assert.False(isEnabled)
+}
diff --git a/tests/integration/utils/kiali_client.go b/tests/integration/utils/kiali_client.go
index 203d380da..ec28a3478 100644
--- a/tests/integration/utils/kiali_client.go
+++ b/tests/integration/utils/kiali_client.go
@@ -16,6 +16,7 @@ import (
 	"github.com/kiali/kiali/jaeger"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/status"
 	"github.com/kiali/kiali/util/httputil"
 )
 
@@ -611,6 +612,23 @@ func Grafana() (*models.GrafanaInfo, int, error) {
 	}
 }
 
+func IstioApiEnabled() (bool, error) {
+	url := fmt.Sprintf("%s/api/status", client.kialiURL)
+	body, _, _, err := httputil.HttpGet(url, client.GetAuth(), TIMEOUT, nil, client.kialiCookies)
+
+	if err == nil {
+		status := new(status.StatusInfo)
+		err = json.Unmarshal(body, &status)
+		if err == nil {
+			return status.IstioEnvironment.IstioAPIEnabled, nil
+		} else {
+			return false, err
+		}
+	} else {
+		return false, err
+	}
+}
+
 func FirstPodName(name, namespace string) (string, error) {
 	workload, _, err := WorkloadDetails(name, namespace)
 	if err == nil {