Repository: kiali/kiali

Issue:
Refactor out global variables from business package
### What do you want to improve?

This is a "tech debt" issue. The business package has a number of global variables that must be set in order to use the package: https://github.com/kiali/kiali/blob/9a11e24d993ef34be0de7fbad3d6df946a530bfa/business/layer.go#L38-L44

Most of the api handlers call `business.Get` which uses these globals and then calls `NewWithBackends` that also uses these globals:
https://github.com/kiali/kiali/blob/9a11e24d993ef34be0de7fbad3d6df946a530bfa/business/layer.go#L83-L115

The problems for this are apparent when writing unit tests for and with the business package. When you write a test that doesn't set all the right globals, your test will panic and fail. It's not always obvious what you need to set and how. This makes it much harder to write tests and discourages folks from writing tests. Another issue is that having global variables lends itself to having global locks alongside those variables to prevent race conditions with multiple different actors trying to use those global variables.

### What is the new behavior?

Instead of having global variables in the business package, those variables can be passed into the business package when creating a new layer. Create a new function to create a `business.Layer` and migrate to the new method over time. Something like
```
NewLayer(cf kubernetes.ClientFactory, cache cache.KialiCache, conf config.Config, prom prometheus.ClientInterface) *Layer {
  	temporaryLayer := &Layer{}

        userClients := cf.GetClients()
	temporaryLayer.App = AppService{prom: prom, userClients: userClients, businessLayer: temporaryLayer}
        ...

	return temporaryLayer
```

Notice that almost all the arguments are interfaces so the "fake" implementations can easily be passed in testing.

The http handlers would need to pass these into the `NewLayer` function when they access the business layer. Although the handlers need to conform to `http.HandlerFunc`, it is a common pattern to wrap handler methods in a closure to pass dependencies into them. So this:
```
func AppList(w http.ResponseWriter, r *http.Request) {
  // Uses globals
  business.Get()
}
```
would become:
```
func AppList(cf kubernetes.ClientFactory, cache cache.KialiCache, conf config.Config, prom prometheus.ClientInterface) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    // Does not use globals
    business.NewLayer(cf, cache, conf, prom)
  }
}
```

Adding the `AppList` handler to the route becomes:
```
		{
			"AppList",
			"GET",
			"/api/namespaces/{namespace}/apps",
			handlers.AppList(cf, cache, conf, prom),
			true,
		},
```

Using this approach we can also migrate each handler individually rather than all at once.

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/molecules.yml b/.github/workflows/molecules.yml
index f5a50a5e8..c4edb8bd3 100644
--- a/.github/workflows/molecules.yml
+++ b/.github/workflows/molecules.yml
@@ -58,7 +58,34 @@ jobs:
             *) echo "Invalid schedule or unknown trigger! Cannot determine Istio version." && exit 1 ;;
           esac
         fi
-        LATEST_ISTIO_VERSIONS="$(curl -s https://api.github.com/repos/istio/istio/releases | jq -r '.[].tag_name' | sort -rV | awk -F. '!seen[$1"."$2]++' | head -n $((OFFSET + 1)))"
+        LATEST_ISTIO_VERSIONS="$(curl -s https://api.github.com/repos/istio/istio/releases | jq -r '.[].tag_name' | sort -rV | awk -F'[-.]' '
+          {
+            minor = $1"."$2
+            is_rc = ($4 == "rc")
+            z = is_rc ? $3 : $3
+            rc_ver = is_rc ? $5 : ""
+
+            if (!minor_ga[minor] && !minor_rc[minor]) {
+              if (is_rc) { minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver }
+              else { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+            } else if (!is_rc && minor_ga[minor]) {
+              if (z > minor_ga_z[minor]) { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+            } else if (!is_rc && !minor_ga[minor]) {
+              minor_ga[minor] = $0; minor_ga_z[minor] = z; delete minor_rc[minor]; delete minor_rc_z[minor]; delete minor_rc_rc[minor]
+            } else if (is_rc && !minor_ga[minor]) {
+              current_rc_stored = minor_rc[minor]
+              split(current_rc_stored, current_rc_parts, "[-.]")
+              current_rc_num_stored = current_rc_parts[5]
+
+              if (!minor_rc[minor] || z > minor_rc_z[minor] || (z == minor_rc_z[minor] && rc_ver > current_rc_num_stored)) {
+                minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver
+              }
+            }
+          }
+          END {
+            for (m in minor_ga) { print minor_ga[m] }
+            for (m in minor_rc) { print minor_rc[m] }
+          }' | sort -Vr | grep -v '^$' | head -n $((OFFSET + 1)))"
         ISTIO_VERSION=$(echo "${LATEST_ISTIO_VERSIONS}" | tail -n 1)
         echo "The latest Istio versions are:"
         echo "${LATEST_ISTIO_VERSIONS}"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 535f81df1..4b0f42a36 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -40,7 +40,7 @@ jobs:
     - name: Checkout code
       uses: actions/checkout@v4
       with:
-        ref: ${{ github.ref_name }}
+        ref: ${{ github.event.inputs.release_branch || github.ref_name }}
 
     - name: Prepare scripts
       run: |
@@ -211,7 +211,7 @@ jobs:
     - name: Checkout code
       uses: actions/checkout@v4
       with:
-        ref: ${{ github.ref_name }}
+        ref: ${{ github.event.inputs.release_branch || github.ref_name }}
 
     - name: Set version to release
       run: |
@@ -261,9 +261,39 @@ jobs:
       run: |
         gh release create $RELEASE_VERSION -t "Kiali $RELEASE_VERSION"
 
-    - name: Create or update version branch
+    - name: Check if branch exists
+      run: |
+        if git ls-remote --exit-code --heads origin "$BRANCH_VERSION"; then
+          echo "EXISTS=true" >> $GITHUB_ENV
+        else
+          echo "EXISTS=false" >> $GITHUB_ENV
+        fi
+
+    - name: Create version branch
+      if: env.EXISTS == 'false'
       run: git push origin $(git rev-parse HEAD):refs/heads/$BRANCH_VERSION
 
+    - name: Create PR to update version branch
+      if: env.EXISTS == 'true'
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: |
+        git checkout -b update-$BRANCH_VERSION-$GITHUB_RUN_ID
+
+        sed -i -r "s/^VERSION \?= (.*)/VERSION \?= $RELEASE_VERSION/" Makefile
+        jq -r ".version |= \"${RELEASE_VERSION:1}\"" frontend/package.json > frontend/package.json.tmp
+        mv frontend/package.json.tmp frontend/package.json
+
+        git add Makefile frontend/package.json
+        git commit -m "Prepare for next version"
+
+        git push origin update-$BRANCH_VERSION-$GITHUB_RUN_ID
+
+        gh pr create --title "Update $BRANCH_VERSION" \
+                    --body "Automated PR to update $BRANCH_VERSION" \
+                    --base "$BRANCH_VERSION" \
+                    --head update-$BRANCH_VERSION-$GITHUB_RUN_ID
+
     - name: Create a PR to prepare for next version
       env:
         BUILD_TAG: kiali-release-${{ github.run_number }}-main
diff --git a/.github/workflows/test-istio-version.yml b/.github/workflows/test-istio-version.yml
index e9796f364..46246d94e 100644
--- a/.github/workflows/test-istio-version.yml
+++ b/.github/workflows/test-istio-version.yml
@@ -49,7 +49,34 @@ jobs:
               *) echo "Invalid schedule or unknown trigger! Cannot determine Istio version." && exit 1 ;;
             esac
           fi
-          LATEST_ISTIO_VERSIONS="$(curl -s https://api.github.com/repos/istio/istio/releases | jq -r '.[].tag_name' | sort -rV | awk -F. '!seen[$1"."$2]++' | head -n $((OFFSET + 1)))"
+          LATEST_ISTIO_VERSIONS="$(curl -s https://api.github.com/repos/istio/istio/releases | jq -r '.[].tag_name' | sort -rV | awk -F'[-.]' '
+            {
+              minor = $1"."$2
+              is_rc = ($4 == "rc")
+              z = is_rc ? $3 : $3
+              rc_ver = is_rc ? $5 : ""
+
+              if (!minor_ga[minor] && !minor_rc[minor]) {
+                if (is_rc) { minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver }
+                else { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+              } else if (!is_rc && minor_ga[minor]) {
+                if (z > minor_ga_z[minor]) { minor_ga[minor] = $0; minor_ga_z[minor] = z }
+              } else if (!is_rc && !minor_ga[minor]) {
+                minor_ga[minor] = $0; minor_ga_z[minor] = z; delete minor_rc[minor]; delete minor_rc_z[minor]; delete minor_rc_rc[minor]
+              } else if (is_rc && !minor_ga[minor]) {
+                current_rc_stored = minor_rc[minor]
+                split(current_rc_stored, current_rc_parts, "[-.]")
+                current_rc_num_stored = current_rc_parts[5]
+
+                if (!minor_rc[minor] || z > minor_rc_z[minor] || (z == minor_rc_z[minor] && rc_ver > current_rc_num_stored)) {
+                  minor_rc[minor] = $0; minor_rc_z[minor] = z; minor_rc_rc[minor] = rc_ver
+                }
+              }
+            }
+            END {
+              for (m in minor_ga) { print minor_ga[m] }
+              for (m in minor_rc) { print minor_rc[m] }
+            }' | sort -Vr | grep -v '^$' | head -n $((OFFSET + 1)))"
           ISTIO_VERSION=$(echo "${LATEST_ISTIO_VERSIONS}" | tail -n 1)
           echo "The latest Istio versions are:"
           echo "${LATEST_ISTIO_VERSIONS}"
diff --git a/business/metrics.go b/business/metrics.go
index b3fa03270..814e262ac 100644
--- a/business/metrics.go
+++ b/business/metrics.go
@@ -402,3 +402,30 @@ func (in *MetricsService) GetZtunnelMetrics(q models.IstioMetricsQuery) (models.
 
 	return metrics, nil
 }
+
+func (in *MetricsService) GetResourceMetrics(q models.IstioMetricsQuery) (models.MetricsMap, error) {
+
+	metrics := make(models.MetricsMap)
+	var err error
+	var converted []models.Metric
+
+	// Component memory usage memory_usage
+	labels := fmt.Sprintf("{pod=~\"%s-.*\"}", q.App)
+	metric := in.prom.FetchRange("container_memory_working_set_bytes", labels, "pod", "sum", &q.RangeQuery)
+	converted, err = models.ConvertMetric("container_memory_working_set_bytes", metric, models.ConversionParams{Scale: 0.000001})
+	if err != nil {
+		return nil, err
+	}
+	metrics["memory_usage"] = append(metrics["container_memory_working_set_bytes"], converted...)
+
+	// Component cpu_usage
+	metricName := fmt.Sprintf("irate(container_cpu_usage_seconds_total{pod=~\"%s-.*\"}[%s])", q.App, q.RateInterval)
+	metric = in.prom.FetchRange(metricName, "", "pod", "sum", &q.RangeQuery)
+	converted, err = models.ConvertMetric(metricName, metric, models.ConversionParams{Scale: 1})
+	if err != nil {
+		return nil, err
+	}
+	metrics["cpu_usage"] = append(metrics[metricName], converted...)
+
+	return metrics, nil
+}
diff --git a/config/config.go b/config/config.go
index 60f9c6dc5..c0ac75f59 100644
--- a/config/config.go
+++ b/config/config.go
@@ -122,7 +122,7 @@ func (fn FeatureName) IsValid() error {
 	case FeatureLogView:
 		return nil
 	}
-	return fmt.Errorf("Invalid feature name: %v", fn)
+	return fmt.Errorf("invalid feature name: %v", fn)
 }
 
 // Global configuration for the application.
@@ -514,14 +514,8 @@ type GraphFindOption struct {
 
 // GraphSettings affect the graph visualization.
 // Animation: animation type point (default) | dash
-// FontLabel: font used for node text (edge label font is determined from this value) TODO: Cytoscape only - remove when cytoscape is removed
-// MinFontBadge: smallest effective font (zoomed font) before removing node badges TODO: Cytoscape only - remove when cytoscape is removed
-// MinFontLabel: smallest effective node text font (zoomed font) before removing labels TODO: Cytoscape only - remove when cytoscape is removed
 type GraphSettings struct {
-	Animation    string  `yaml:"animation,omitempty" json:"animation,omitempty"`
-	FontLabel    float32 `yaml:"font_label,omitempty" json:"fontLabel,omitempty"`
-	MinFontBadge float32 `yaml:"min_font_badge,omitempty" json:"minFontBadge,omitempty"`
-	MinFontLabel float32 `yaml:"min_font_label,omitempty" json:"minFontLabel,omitempty"`
+	Animation string `yaml:"animation,omitempty" json:"animation,omitempty"`
 }
 
 // GraphTraffic defines the protocol-specific rates used to determine traffic for graph generation.
@@ -540,7 +534,6 @@ type GraphTraffic struct {
 type GraphUIDefaults struct {
 	FindOptions []GraphFindOption `yaml:"find_options,omitempty" json:"findOptions,omitempty"`
 	HideOptions []GraphFindOption `yaml:"hide_options,omitempty" json:"hideOptions,omitempty"`
-	Impl        string            `yaml:"impl,omitempty" json:"impl,omitempty"` // TODO: remove when cytoscape is removed
 	Settings    GraphSettings     `yaml:"settings,omitempty" json:"settings,omitempty"`
 	Traffic     GraphTraffic      `yaml:"traffic,omitempty" json:"traffic,omitempty"`
 }
@@ -856,12 +849,8 @@ func NewConfig() (c *Config) {
 							Expression:  "rank > 2",
 						},
 					},
-					Impl: "pf",
 					Settings: GraphSettings{
-						Animation:    "point",
-						FontLabel:    13,
-						MinFontBadge: 7,
-						MinFontLabel: 10,
+						Animation: "point",
 					},
 					Traffic: GraphTraffic{
 						Ambient: "total",
@@ -1393,7 +1382,7 @@ func Validate(conf Config) error {
 		auth.Strategy != AuthStrategyOpenshift &&
 		auth.Strategy != AuthStrategyToken &&
 		auth.Strategy != AuthStrategyHeader {
-		return fmt.Errorf("Invalid authentication strategy [%v]", auth.Strategy)
+		return fmt.Errorf("invalid authentication strategy [%v]", auth.Strategy)
 	}
 
 	// Check the ciphering key for sessions
diff --git a/doc.go b/doc.go
index 4a27408f4..ce9294f65 100644
--- a/doc.go
+++ b/doc.go
@@ -1,7 +1,7 @@
 package main
 
 import (
-	"github.com/kiali/kiali/graph/config/cytoscape"
+	config_common "github.com/kiali/kiali/graph/config/common"
 	"github.com/kiali/kiali/handlers/authentication"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/models"
@@ -583,11 +583,11 @@ type SwaggTokenGeneratedResp struct {
 	Body authentication.UserSessionData
 }
 
-// HTTP status code 200 and cytoscapejs Config in data
+// HTTP status code 200 and graph Config in data
 // swagger:response graphResponse
 type GraphResponse struct {
 	// in:body
-	Body cytoscape.Config
+	Body config_common.Config
 }
 
 // HTTP status code 200 and IstioConfigList model in data
diff --git a/frontend/cypress/integration/common/app_details_multicluster-cy.ts b/frontend/cypress/integration/common/app_details_multicluster-cy.ts
deleted file mode 100644
index 917f8929c..000000000
--- a/frontend/cypress/integration/common/app_details_multicluster-cy.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { Given, Step, Then, When } from '@badeball/cypress-cucumber-preprocessor';
-import { nodeInfo } from './graph-pf';
-
-Then(
-  'user sees {string} from a remote {string} cluster in the cytoscape minigraph',
-  (type: string, cluster: string) => {
-    cy.waitForReact();
-    cy.getReact('CytoscapeGraph')
-      .should('have.length', '1')
-      .getCurrentState()
-      .then(state => {
-        const apps = state.cy.nodes(`[cluster="${cluster}"][nodeType="${type}"][namespace="bookinfo"]`).length;
-        assert.isAbove(apps, 0);
-      });
-  }
-);
-
-// And user clicks on the "reviews" <type> from the "west" cluster visible in the graph
-Given(
-  'the {string} {string} from the {string} cluster is visible in the cytoscape minigraph',
-  (name: string, type: string, cluster: string) => {
-    Step(this, 'user sees a cytoscape minigraph');
-    cy.waitForReact();
-    cy.getReact('CytoscapeGraph')
-      .should('have.length', '1')
-      .then($graph => {
-        cy.wrap($graph)
-          .getProps()
-          .then(props => {
-            const graphType = props.graphData.fetchParams.graphType;
-            const { nodeType, isBox } = nodeInfo(type, graphType);
-            cy.wrap($graph)
-              .getCurrentState()
-              .then(state => {
-                cy.wrap(
-                  state.cy
-                    .nodes()
-                    .some(
-                      node =>
-                        node.data('nodeType') === nodeType &&
-                        node.data('namespace') === 'bookinfo' &&
-                        node.data(type) === name &&
-                        node.data('cluster') === cluster &&
-                        node.data('isBox') === isBox
-                    )
-                ).should('be.true');
-              });
-          });
-      });
-  }
-);
-
-When(
-  'user clicks on the {string} {string} from the {string} cluster in the cytoscape minigraph',
-  (name: string, type: string, cluster: string) => {
-    cy.waitForReact();
-    cy.getReact('CytoscapeGraph')
-      .should('have.length', '1')
-      .then($graph => {
-        cy.wrap($graph)
-          .getProps()
-          .then(props => {
-            const graphType = props.graphData.fetchParams.graphType;
-            cy.wrap($graph)
-              .getCurrentState()
-              .then(state => {
-                const node = state.cy
-                  .nodes()
-                  .toArray()
-                  .find(node => {
-                    const { nodeType, isBox } = nodeInfo(type, graphType);
-                    return (
-                      node.data('nodeType') === nodeType &&
-                      node.data('namespace') === 'bookinfo' &&
-                      node.data(type) === name &&
-                      node.data('cluster') === cluster &&
-                      node.data('isBox') === isBox &&
-                      !node.data('isInaccessible')
-                    );
-                  });
-                node.emit('tap');
-              });
-          });
-      });
-  }
-);
diff --git a/frontend/cypress/integration/common/authorization-cy.ts b/frontend/cypress/integration/common/authorization-cy.ts
deleted file mode 100644
index 7c82ec4fa..000000000
--- a/frontend/cypress/integration/common/authorization-cy.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Then } from '@badeball/cypress-cucumber-preprocessor';
-import { GraphDataSource } from 'services/GraphDataSource';
-
-Then('the nodes on the cytoscape graph located in the {string} cluster should be restricted', (cluster: string) => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const nodes = state.cy.nodes().filter(node => node.data('cluster') === cluster && !node.data('isBox'));
-          nodes.forEach(node => {
-            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-            expect(node.data('isInaccessible')).to.be.true;
-          });
-        });
-    });
-});
-
-Then('the nodes on the cytoscape minigraph located in the {string} cluster should be restricted', (cluster: string) => {
-  cy.waitForReact();
-  cy.getReact('MiniGraphCardComponent')
-    .getProps('dataSource')
-    .should((dataSource: GraphDataSource) => {
-      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-      expect(dataSource.isLoading).to.be.false;
-    })
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const nodes = state.cy.nodes().filter(node => node.data('cluster') === cluster && !node.data('isBox'));
-          nodes.forEach(node => {
-            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
-            expect(node.data('isInaccessible')).to.be.true;
-          });
-        });
-    });
-});
diff --git a/frontend/cypress/integration/common/graph-cy.ts b/frontend/cypress/integration/common/graph-cy.ts
deleted file mode 100644
index 40b158405..000000000
--- a/frontend/cypress/integration/common/graph-cy.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
-  This file contains graph related step definitions
-  that are common to multiple features.
-*/
-
-import { Then } from '@badeball/cypress-cucumber-preprocessor';
-
-Then('user does not see a cytoscape minigraph', () => {
-  cy.get('#MiniGraphCard').find('h5').contains('Empty Graph');
-});
-
-Then('user sees a cytoscape minigraph', () => {
-  cy.getBySel('mini-graph').within(() => {
-    cy.get('#cytoscape-graph').should('be.visible');
-    cy.get('#cy').should('be.visible');
-  });
-});
-
-Then(
-  'user sees the {string} namespace deployed across the east and west clusters in the cytoscape graph',
-  (namespace: string) => {
-    cy.waitForReact();
-    cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-      .should('have.length', '1')
-      .then(() => {
-        cy.getReact('CytoscapeGraph')
-          .should('have.length', '1')
-          .getCurrentState()
-          .then(state => {
-            const namespaceBoxes = state.cy
-              .nodes()
-              .filter(node => node.data('isBox') === 'namespace' && node.data('namespace') === namespace);
-            expect(namespaceBoxes.length).to.equal(2);
-            expect(namespaceBoxes.filter(node => node.data('cluster') === 'east').length).to.equal(1);
-            expect(namespaceBoxes.filter(node => node.data('cluster') === 'west').length).to.equal(1);
-          });
-      });
-  }
-);
-
-Then(
-  'nodes in the {string} cluster in the cytoscape graph should contain the cluster name in their links',
-  (cluster: string) => {
-    cy.waitForReact();
-    cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-      .should('have.length', '1')
-      .then(() => {
-        cy.getReact('CytoscapeGraph')
-          .should('have.length', '1')
-          .getCurrentState()
-          .then(state => {
-            const nodes = state.cy.nodes().filter(node => node.data('cluster') === cluster);
-            nodes.forEach(node => {
-              const links = node.connectedEdges().filter(edge => edge.data('source') === node.id());
-              links.forEach(link => {
-                const sourceNode = nodes.toArray().find(node => node.id() === link.data('source'));
-                expect(sourceNode.data('cluster')).to.equal(cluster);
-              });
-            });
-          });
-      });
-  }
-);
-
-Then(
-  'user clicks on the {string} workload in the {string} namespace in the {string} cluster in the cytoscape graph',
-  (workload: string, namespace: string, cluster: string) => {
-    cy.waitForReact();
-    cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-      .should('have.length', '1')
-      .then(() => {
-        cy.getReact('CytoscapeGraph')
-          .should('have.length', '1')
-          .getCurrentState()
-          .then(state => {
-            const workloadNode = state.cy.nodes().filter(
-              node =>
-                // Apparently workloads are apps for the versioned app graph.
-                node.data('nodeType') === 'app' &&
-                node.data('isBox') === undefined &&
-                node.data('workload') === workload &&
-                node.data('namespace') === namespace &&
-                node.data('cluster') === cluster
-            );
-            expect(workloadNode.length).to.equal(1);
-            cy.wrap(workloadNode.emit('tap')).then(() => {
-              // Wait for the side panel to change.
-              // Note we can't use summary-graph-panel since that
-              // element will get unmounted and disappear when
-              // the context changes but the graph-side-panel does not.
-              cy.get('#graph-side-panel').contains(workload);
-            });
-          });
-      });
-  }
-);
diff --git a/frontend/cypress/integration/common/graph_context_menu-cy.ts b/frontend/cypress/integration/common/graph_context_menu-cy.ts
deleted file mode 100644
index 35370b9d9..000000000
--- a/frontend/cypress/integration/common/graph_context_menu-cy.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { Then, When } from '@badeball/cypress-cucumber-preprocessor';
-import { ensureKialiFinishedLoading } from './transition';
-
-// Single cluster only.
-When('user opens the context menu of the {string} service node in the cytoscape graph', (svcName: string) => {
-  ensureKialiFinishedLoading();
-  cy.waitForReact();
-
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const node = state.cy.nodes(`[nodeType="service"][service="${svcName}"]`);
-          node.emit('cxttapstart');
-          cy.wrap(node).as('contextNode');
-        });
-    });
-});
-
-When(
-  'user opens the context menu of the {string} service node on the {string} cluster in the cytoscape graph',
-  (svcName: string, cluster: string) => {
-    ensureKialiFinishedLoading();
-    cy.waitForReact();
-    cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-      .should('have.length', '1')
-      .then(() => {
-        cy.getReact('CytoscapeGraph')
-          .should('have.length', '1')
-          .getCurrentState()
-          // Using should we can retry until the node is found.
-          // It could be that a node has not yet appeared on the traffic map
-          // because traffic hasn't made it to the node yet. So, we retry
-          // and refresh the traffic map until it shows up.
-          .should(state => {
-            const node = state.cy.nodes(
-              `[nodeType="service"][service="${svcName}"][cluster="${cluster}"][namespace="bookinfo"]`
-            );
-            if (node.length === 0) {
-              Cypress.$('[data-test="refresh-button"]').trigger('click');
-              throw new Error(`service Node ${svcName} in namespace bookinfo in cluster ${cluster} not found`);
-            }
-            expect(node.length).to.equal(1);
-          })
-          .then(state => {
-            // Wait for the last "click" to finish before continuing.
-            ensureKialiFinishedLoading();
-            const node = state.cy.nodes(
-              `[nodeType="service"][service="${svcName}"][cluster="${cluster}"][namespace="bookinfo"]`
-            );
-            expect(node.length).to.equal(1);
-            node.emit('cxttapstart');
-            cy.wrap(node).as('contextNode');
-            cy.getBySel('graph-node-context-menu').should('be.visible');
-          });
-      });
-  }
-);
-
-When('user clicks the {string} item of the context menu in the cytoscape graph', (menuKey: string) => {
-  cy.get(`[data-test="graph-node-context-menu"] [data-test="${menuKey}"]`).then($item => {
-    cy.wrap($item).click();
-  });
-});
-
-When('user clicks the {string} action of the context menu in the cytoscape graph', (actionKey: string) => {
-  cy.get(`[data-test="graph-node-context-menu"] [data-test="${actionKey}_action"]`).then($item => {
-    cy.wrap($item).click();
-  });
-});
-
-Then(
-  'user should see no cluster parameter in the url when clicking the {string} link in the context menu in the cytoscape graph',
-  (linkText: string) => {
-    cy.get(`[data-test="graph-node-context-menu"]`).within(() => {
-      cy.get('a')
-        .contains(linkText)
-        .click()
-        .then(() => {
-          cy.url().should('not.include', 'clusterName=');
-          cy.go('back');
-        });
-    });
-  }
-);
-
-Then(
-  'user should see the {string} cluster parameter in the url when clicking the {string} link in the context menu in the cytoscape graph',
-  (cluster: string, linkText: string) => {
-    cy.get(`[data-test="graph-node-context-menu"]`).within(() => {
-      cy.get('a')
-        .contains(linkText)
-        .click()
-        .then(() => {
-          cy.url().should('include', `clusterName=${cluster}`);
-          cy.go('back');
-        });
-    });
-  }
-);
-
-Then('configuration is duplicated to the {string} cluster in the cytoscape graph', (cluster: string) => {
-  cy.get('@contextNode').then((node: any) => {
-    const namespace = node.data('namespace');
-    const service = node.data('service');
-
-    cy.fixture(`${service}-virtualservice.json`).then(virtualService => {
-      cy.request({
-        url: `api/namespaces/${namespace}/istio/networking.istio.io/v1/VirtualService`,
-        method: 'POST',
-        qs: { clusterName: cluster },
-        body: virtualService
-      });
-    });
-
-    cy.fixture(`${service}-destinationrule.json`).then(destinationRule => {
-      cy.request({
-        url: `api/namespaces/${namespace}/istio/networking.istio.io/v1/DestinationRule`,
-        method: 'POST',
-        qs: { clusterName: cluster },
-        body: destinationRule
-      });
-    });
-  });
-});
diff --git a/frontend/cypress/integration/common/graph_display-cy.ts b/frontend/cypress/integration/common/graph_display-cy.ts
deleted file mode 100644
index 5718e59a2..000000000
--- a/frontend/cypress/integration/common/graph_display-cy.ts
+++ /dev/null
@@ -1,282 +0,0 @@
-import { Then, When } from '@badeball/cypress-cucumber-preprocessor';
-import { ensureKialiFinishedLoading } from './transition';
-
-When('user graphs {string} namespaces in the cytoscape graph', (namespaces: string) => {
-  // Forcing "Pause" to not cause unhandled promises from the browser when cypress is testing
-  cy.intercept(`**/api/namespaces/graph*`).as('graphNamespaces');
-
-  cy.visit({ url: `/console/graph/namespaces?refresh=0&namespaces=${namespaces}` });
-
-  if (namespaces !== '') {
-    cy.wait('@graphNamespaces');
-  }
-
-  ensureKialiFinishedLoading();
-});
-
-Then('the cytoscape graph reflects default settings', () => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          // no nonDefault edge label info
-          let numEdges = state.cy.edges(`[?responseTime],[?throughput]`).length;
-          assert.equal(numEdges, 0);
-
-          // no idle edges, mtls
-          numEdges = state.cy.edges(`[^hasTraffic],[isMTLS > 0]`).length;
-          assert.equal(numEdges, 0);
-
-          // boxes
-          let numNodes = state.cy.nodes(`[isBox = "app"]`).length;
-          assert.isAbove(numNodes, 0);
-          numNodes = state.cy.nodes(`[isBox = "namespace"]`).length;
-          assert.isAbove(numNodes, 0);
-
-          // service nodes
-          numNodes = state.cy.nodes(`[nodeType = "service"]`).length;
-          assert.isAbove(numNodes, 0);
-
-          // a variety of not-found tests
-          numNodes = state.cy.nodes(`[isBox = "cluster"],[?isIdle],[?rank],[nodeType = "operation"]`).length;
-          assert.equal(numNodes, 0);
-        });
-    });
-});
-
-Then('user sees {string} edge labels in the cytoscape graph', (el: string) => {
-  validateInput(el, 'appear');
-
-  let rate: string;
-  switch (el) {
-    case 'trafficDistribution':
-      rate = 'httpPercentReq';
-      break;
-    case 'trafficRate':
-      rate = 'http';
-      break;
-    default:
-      rate = el;
-  }
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numEdges = state.cy.edges(`[${rate}" > 0]`).length;
-          assert.isAbove(numEdges, 0);
-        });
-    });
-});
-
-Then('user does not see {string} boxing in the cytoscape graph', (boxByType: string) => {
-  validateInput(`boxBy${boxByType}`, 'does not appear');
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numBoxes = state.cy.nodes(`[isBox = "${boxByType.toLowerCase()}"]`).length;
-          assert.equal(numBoxes, 0);
-        });
-    });
-});
-
-// In older versions of Istio, when this was written, this istio-system namespace
-// would typicaly have some idle edges.  That is no longer the case.  It is not easy
-// to force an idle edge to exist, especially in the timeframe of a test, a demo would
-// need to generate an edge, then stop, and wait for it to become idle (i.e. at least a
-// minute depending on the duration used in the test.) In the future we could possibly
-// try to add something like this, but for now I am changing the test to just ensure
-// that non-idle edges don't disappear.
-Then('idle edges {string} in the cytoscape graph', (action: string) => {
-  validateInput('filterIdleEdges', action);
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numEdges = state.cy.edges(`[hasTraffic]`).length;
-          const numIdleEdges = state.cy.edges(`[^hasTraffic]`).length;
-
-          if (action === 'appear') {
-            assert.isAbove(numEdges, 0);
-            assert.isAtLeast(numIdleEdges, 0);
-          } else {
-            assert.isAbove(numEdges, 0);
-            assert.equal(numIdleEdges, 0);
-          }
-        });
-    });
-});
-
-Then('idle nodes {string} in the cytoscape graph', (action: string) => {
-  validateInput('filterIdleNodes', action);
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numNodes = state.cy.nodes(`[?isIdle]`).length;
-          if (action === 'appear') {
-            assert.equal(numNodes, 16);
-          } else {
-            assert.equal(numNodes, 0);
-          }
-        });
-    });
-});
-
-Then('ranks {string} in the cytoscape graph', (action: string) => {
-  validateInput('rank', action);
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numNodes = state.cy.nodes(`[rank > 0]`).length;
-          if (action === 'appear') {
-            assert.isAbove(numNodes, 0);
-          } else {
-            assert.equal(numNodes, 0);
-          }
-        });
-    });
-});
-
-Then('user does not see service nodes in the cytoscape graph', () => {
-  validateInput('filterServiceNodes', 'do not appear');
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numBoxes = state.cy.nodes(`[nodeType = "service"][^isOutside]`).length;
-          assert.equal(numBoxes, 0);
-        });
-    });
-});
-
-Then('security {string} in the cytoscape graph', (action: string) => {
-  validateInput('filterSecurity', action);
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numEdges = state.cy.edges(`[isMTLS > 0]`).length;
-          if (action === 'appears') {
-            assert.isAbove(numEdges, 0);
-          } else {
-            assert.equal(numEdges, 0);
-          }
-        });
-    });
-});
-
-Then('only a single cluster box should be visible in the cytoscape graph', () => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const clusterBoxes = state.cy.nodes(`[isBox = "cluster"]`).length;
-          assert.equal(clusterBoxes, 0);
-
-          const namespaceBoxes = state.cy.nodes(`[isBox = "namespace"][namespace = "bookinfo"]`).length;
-          assert.equal(namespaceBoxes, 1);
-        });
-    });
-});
-
-const validateInput = (option: string, action: string): void => {
-  if (action.startsWith('appear')) {
-    cy.get('div#graph-display-menu')
-      .find(`input#${option}`)
-      .should('exist')
-      .should('be.checked')
-      .should('not.be.disabled'); // this forces a wait, enables when graph is refreshed
-  } else {
-    cy.get('div#graph-display-menu')
-      .find(`input#${option}`)
-      .should('exist')
-      .should('not.be.checked')
-      .should('not.be.disabled'); // this forces a wait, enables when graph is refreshed
-  }
-};
-
-Then(
-  'user double-clicks on the {string} {string} from the {string} cluster in the main cytoscape graph',
-  (name: string, type: string, cluster: string) => {
-    cy.waitForReact();
-    cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-      .should('have.length', '1')
-      .then(() => {
-        cy.getReact('CytoscapeGraph')
-          .should('have.length', '1')
-          .getCurrentState()
-          .then(state => {
-            let node;
-            if (type === 'app') {
-              node = state.cy.nodes(`[app="${name}"][cluster="${cluster}"][isBox="app"]`);
-            } else if (type === 'service') {
-              node = state.cy.nodes(`[nodeType="service"][cluster="${cluster}"][app="${name}"]`);
-            }
-            // none of the standard cytoscape.js events for double-clicks were not working unfortunately
-            node.emit('tap');
-            node.emit('tap');
-          });
-      });
-  }
-);
-
-Then('{int} edges appear in the cytoscape graph', (edges: number) => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numEdges = state.cy.edges(`[hasTraffic]`).length;
-          // It can be more, depending on the service version redirection
-          assert.isAtLeast(numEdges, edges);
-        });
-    });
-});
diff --git a/frontend/cypress/integration/common/graph_display-pf.ts b/frontend/cypress/integration/common/graph_display-pf.ts
index 70363dda2..62f9dfc8f 100644
--- a/frontend/cypress/integration/common/graph_display-pf.ts
+++ b/frontend/cypress/integration/common/graph_display-pf.ts
@@ -35,6 +35,8 @@ When('user {string} {string} edge labels', (action: string, edgeLabel: string) =
 });
 
 When('user {string} {string} option', (action: string, option: string) => {
+  cy.intercept(`**/api/namespaces/graph*`).as('graphNamespaces');
+
   switch (option.toLowerCase()) {
     case 'cluster boxes':
       option = 'boxByCluster';
@@ -69,6 +71,9 @@ When('user {string} {string} option', (action: string, option: string) => {
     case 'virtual services':
       option = 'filterVS';
       break;
+    case 'waypoint proxies':
+      option = 'filterWaypoints';
+      break;
     default:
       option = 'xxx';
   }
@@ -79,9 +84,13 @@ When('user {string} {string} option', (action: string, option: string) => {
     if (option === 'rank') {
       cy.get(`input#inboundEdges`).check();
     }
+    if (option === 'filterWaypoints') {
+      cy.wait('@graphNamespaces');
+    }
   } else {
     cy.get('div#graph-display-menu').find(`input#${option}`).uncheck();
   }
+  ensureKialiFinishedLoading();
 });
 
 When('user resets to factory default', () => {
@@ -520,3 +529,24 @@ Then('the {string} node {string} exists', (nodeName: string, action: string) =>
       }
     });
 });
+
+Then('the {string} service {string} exists', (serviceName: string, action: string) => {
+  cy.waitForReact();
+  cy.getReact('GraphPagePFComponent', { state: { isReady: true } })
+    .should('have.length', 1)
+    .then($graph => {
+      const { state } = $graph[0];
+
+      const controller = state.graphRefs.getController() as Visualization;
+      assert.isTrue(controller.hasGraph());
+      const { nodes } = elems(controller);
+
+      const foundNode = nodes.filter(node => node.getData().service === serviceName);
+
+      if (action === 'does') {
+        assert.equal(foundNode.length, 1);
+      } else {
+        assert.equal(foundNode.length, 0);
+      }
+    });
+});
diff --git a/frontend/cypress/integration/common/graph_find_hide-cy.ts b/frontend/cypress/integration/common/graph_find_hide-cy.ts
deleted file mode 100644
index 7471ca973..000000000
--- a/frontend/cypress/integration/common/graph_find_hide-cy.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { Then } from '@badeball/cypress-cucumber-preprocessor';
-
-Then('user sees unhealthy workloads highlighted on the cytoscape graph', () => {
-  const expectedUnhealthyNodes = [
-    {
-      app: 'v-server',
-      version: 'v1',
-      namespace: 'alpha'
-    },
-    {
-      app: 'w-server',
-      version: 'v1',
-      namespace: 'alpha'
-    },
-    {
-      app: 'w-server',
-      version: undefined, // Service does not have version
-      namespace: 'alpha'
-    }
-  ];
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const unhealthyNodes = state.cy
-            .nodes()
-            .filter((node: any) => node.classes().includes('find'))
-            .map((node: any) => ({
-              app: node.data('app'),
-              version: node.data('version'),
-              namespace: node.data('namespace')
-            }));
-          expect(unhealthyNodes).to.include.deep.members(expectedUnhealthyNodes);
-        });
-    });
-});
-
-Then('user sees nothing highlighted on the cytoscape graph', () => {
-  cy.contains('Loading Graph').should('not.exist');
-
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          expect(state.cy.nodes().filter((node: any) => node.classes().includes('find')).length).to.equal(0);
-        });
-    });
-});
-
-Then('user sees no unhealthy workloads on the cytoscape graph', () => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const noUnhealthy = state.cy
-            .nodes()
-            // Unhealthy boxes are fine.
-            .every((node: any) => node.data('healthStatus') !== 'Failure' || node.data('nodeType') === 'box');
-
-          expect(noUnhealthy).to.equal(true);
-        });
-    });
-});
-
-Then('user sees no healthy workloads on the cytoscape graph', () => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const noHealthy = state.cy
-            .nodes()
-            .every((node: any) => node.data('healthStatus') !== 'Healthy' || node.data('nodeType') === 'box');
-
-          expect(noHealthy).to.equal(true);
-        });
-    });
-});
diff --git a/frontend/cypress/integration/common/graph_side_panel-cy.ts b/frontend/cypress/integration/common/graph_side_panel-cy.ts
deleted file mode 100644
index 4932d5191..000000000
--- a/frontend/cypress/integration/common/graph_side_panel-cy.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { When } from '@badeball/cypress-cucumber-preprocessor';
-
-When('user clicks the {string} {string} node in the cytoscape graph', (svcName: string, nodeType: string) => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const node = state.cy.nodes(`[nodeType="${nodeType}"][${nodeType}="${svcName}"]`);
-          node.emit('tap');
-        });
-    });
-});
-
-When(
-  'user clicks the edge from {string} {string} to {string} {string} in the cytoscape graph',
-  (svcName: string, nodeType: string, destSvcName: string, destNodeType: string) => {
-    cy.waitForReact();
-    cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-      .should('have.length', '1')
-      .then(() => {
-        cy.getReact('CytoscapeGraph')
-          .should('have.length', '1')
-          .getCurrentState()
-          .then(state => {
-            const node = state.cy.nodes(`[nodeType="${nodeType}"][${nodeType}="${svcName}"]`);
-            const destNode = state.cy.nodes(`[nodeType="${destNodeType}"][${destNodeType}="${destSvcName}"]`);
-            const edge = state.cy.edges(`[source="${node.id()}"][target="${destNode.id()}"]`);
-
-            edge.emit('tap');
-          });
-      });
-  }
-);
-
-When(
-  'user clicks the {string} service node in the {string} namespace in the {string} cluster in the cytoscape graph',
-  (service: string, namespace: string, cluster: string) => {
-    cy.waitForReact();
-    cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-      .should('have.length', '1')
-      .then(() => {
-        cy.getReact('CytoscapeGraph')
-          .should('have.length', '1')
-          .getCurrentState()
-          .then($graph => {
-            const serviceNode = $graph.cy
-              .nodes()
-              .filter(
-                node =>
-                  node.data('nodeType') === 'service' &&
-                  node.data('isBox') === undefined &&
-                  node.data('service') === service &&
-                  node.data('namespace') === namespace &&
-                  node.data('cluster') === cluster
-              );
-            expect(serviceNode.length).to.equal(1);
-            cy.wrap(serviceNode.emit('tap')).then(() => {
-              // Wait for the side panel to change.
-              // Note we can't use summary-graph-panel since that
-              // element will get unmounted and disappear when
-              // the context changes but the graph-side-panel does not.
-              cy.get('#graph-side-panel').contains(service);
-              cy.wrap(serviceNode).as('contextNode');
-            });
-          });
-      });
-  }
-);
-
-When('user chooses to delete the routing in the cystoscape graph', () => {
-  cy.get('@contextNode').then(node => {
-    const cluster = node.data('cluster');
-    const service = node.data('service');
-    const namespace = node.data('namespace');
-    cy.log(`Deleting traffic routing for ${service} service in namespace ${namespace}, data: ${node.data()}`);
-    cy.intercept({
-      pathname: `**/api/namespaces/${namespace}/istio/networking.istio.io/v1/VirtualService/${service}`,
-      method: 'DELETE',
-      query: { clusterName: cluster }
-    }).as('delete-vs');
-    cy.intercept({
-      pathname: `**/api/namespaces/${namespace}/istio/networking.istio.io/v1/DestinationRule/${service}`,
-      method: 'DELETE',
-      query: { clusterName: cluster }
-    }).as('delete-dr');
-
-    cy.getBySel('confirm-delete').click();
-
-    cy.wait('@delete-vs').its('response.statusCode').should('eq', 200);
-    cy.wait('@delete-dr').its('response.statusCode').should('eq', 200);
-  });
-});
diff --git a/frontend/cypress/integration/common/graph_toolbar-cy.ts b/frontend/cypress/integration/common/graph_toolbar-cy.ts
deleted file mode 100644
index fd454b19a..000000000
--- a/frontend/cypress/integration/common/graph_toolbar-cy.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Then, When } from '@badeball/cypress-cucumber-preprocessor';
-import { CytoscapeGlobalScratchData, CytoscapeGlobalScratchNamespace } from 'types/Graph';
-
-When(
-  'user graphs {string} namespaces with refresh {string} and duration {string} in the cytoscape graph',
-  (namespaces: string, refresh: string, duration: string) => {
-    cy.visit({
-      url: `/console/graph/namespaces?refresh=${refresh}&duration=${duration}&namespaces=${namespaces}`
-    });
-  }
-);
-
-Then('user {string} {string} traffic in the cytoscape graph', (action: string, protocol: string) => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const numEdges = state.cy.edges(`[protocol = "${protocol}"]`).length;
-
-          if (action === 'sees') {
-            assert.isAbove(numEdges, 0);
-          } else {
-            assert.equal(numEdges, 0);
-          }
-        });
-    });
-});
-
-Then('user sees a {string} cytoscape graph', graphType => {
-  cy.waitForReact();
-  cy.getReact('GraphPageComponent', { state: { graphData: { isLoading: false } } })
-    .should('have.length', '1')
-    .then(() => {
-      cy.getReact('CytoscapeGraph')
-        .should('have.length', '1')
-        .getCurrentState()
-        .then(state => {
-          const globalScratch: CytoscapeGlobalScratchData = state.cy.scratch(CytoscapeGlobalScratchNamespace);
-          assert.equal(globalScratch.graphType, graphType);
-        });
-    });
-});
diff --git a/frontend/cypress/integration/common/graph_toolbar_legend-cy.ts b/frontend/cypress/integration/common/graph_toolbar_legend-cy.ts
deleted file mode 100644
index e0ab340fa..000000000
--- a/frontend/cypress/integration/common/graph_toolbar_legend-cy.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Then, When } from '@badeball/cypress-cucumber-preprocessor';
-
-const buttonClick = (label: string): void => {
-  cy.get(`button[aria-label="${label}"]`).click();
-};
-
-const buttonPrepare = (label: string, active: boolean): void => {
-  cy.waitForReact();
-
-  cy.getReact('Button', { props: { 'aria-label': `${label}` } })
-    .should('have.length', '1')
-    .getProps()
-    .then(props => {
-      if (props.isActive !== active) {
-        buttonClick(label);
-      }
-    });
-};
-
-const buttonState = (label: string, active: boolean): void => {
-  cy.waitForReact();
-
-  cy.getReact('Button', { props: { 'aria-label': `${label}` } })
-    .should('have.length', '1')
-    .getProps('isActive')
-    .should('eq', active);
-};
-
-Then('the toggle button {string} is enabled in the cytoscape graph', (label: string) => {
-  cy.get(`button[aria-label="${label}"]`).should('have.attr', 'aria-disabled', 'false');
-});
-
-When('the button {string} is clicked in the cytoscape graph', (label: string) => {
-  buttonClick(label);
-});
-
-Then('the button {string} is active in the cytoscape graph', (label: string) => {
-  buttonState(label, true);
-});
-
-Then('the button {string} is not active in the cytoscape graph', (label: string) => {
-  buttonState(label, false);
-});
-
-Then('the {string} is turned on in the cytoscape graph', (label: string) => {
-  buttonPrepare(label, true);
-});
-
-Then('the {string} is turned off in the cytoscape graph', (label: string) => {
-  buttonPrepare(label, false);
-});
-
-When('the Legend section is visible in the cytoscape graph', () => {
-  buttonClick('Show Legend');
-});
diff --git a/frontend/cypress/integration/common/kiali_login.ts b/frontend/cypress/integration/common/kiali_login.ts
index 6d4c2a437..b10964f8d 100644
--- a/frontend/cypress/integration/common/kiali_login.ts
+++ b/frontend/cypress/integration/common/kiali_login.ts
@@ -48,14 +48,6 @@ Given('user fill in username and password', () => {
   }
 });
 
-Given('user does not fill in username and password', () => {
-  if (auth_strategy === 'openshift') {
-    cy.log('Log in with empty credentials');
-    cy.get('#inputUsername').clear();
-    cy.get('button[type="submit"]').click();
-  }
-});
-
 Given('user fills in an invalid username', () => {
   if (auth_strategy === 'openshift') {
     let invalid = 'foobar';
diff --git a/frontend/cypress/integration/common/waypoint.ts b/frontend/cypress/integration/common/waypoint.ts
index 4ffb8ce55..30624cb30 100644
--- a/frontend/cypress/integration/common/waypoint.ts
+++ b/frontend/cypress/integration/common/waypoint.ts
@@ -75,7 +75,12 @@ Then('the user hovers in the {string} label and sees {string} in the tooltip', (
   cy.get(`[data-test=workload-description-card]`).contains('span', label).trigger('mouseleave');
 });
 
+Then("the user doesn't see a L7 link", () => {
+  cy.get('[data-test=workload-description-card]').should('not.contain', 'L7');
+});
+
 Then('the user sees the L7 {string} link', (waypoint: string) => {
+  cy.get('[data-test=workload-description-card]').should('contain', 'L7');
   cy.get(`[data-test=waypoint-list]`).contains('span', 'L7');
   cy.get(`[data-test=waypoint-link]`).contains('a', waypoint);
 });
@@ -120,6 +125,10 @@ Then('user goes to the waypoint {string} subtab', (subtab: string) => {
   cy.get('#waypoint-details').should('be.visible').contains(subtab).click();
 });
 
+Then("the {string} subtab doesn't exist", (subtab: string) => {
+  cy.get('#waypoint-details').should('be.visible').should('not.contain', subtab);
+});
+
 Then('validates Services data', () => {
   cy.get('[data-test="enrolled-data-title"]').should('be.visible');
   cy.get('[role="grid"]').should('be.visible').get('[data-label="Name"]').and('contain', 'productpage');
@@ -128,8 +137,20 @@ Then('validates Services data', () => {
   cy.get('[role="grid"]').should('be.visible').get('[data-label="Labeled by"]').and('contain', 'namespace');
 });
 
-Then('validates waypoint Info data', () => {
-  cy.get('[data-test=waypointfor-title]').should('exist').and('contain', 'service');
+Then(
+  'validates Services data with {string} rows and {string} workload, {string} namespace, {string} label for, {string} badge',
+  (rows: number, workload: string, ns: string, label: string, badge: string) => {
+    cy.get('[data-test="enrolled-data-title"]').should('be.visible');
+    cy.get('table tbody tr').should('have.length', rows);
+    cy.get('[role="grid"]').should('be.visible').get('[data-label="Name"]').and('contain', workload);
+    cy.get('[role="grid"]').should('be.visible').get(`#${badge}`).should('exist');
+    cy.get('[role="grid"]').should('be.visible').get('[data-label="Namespace"]').and('contain', ns);
+    cy.get('[role="grid"]').should('be.visible').get('[data-label="Labeled by"]').and('contain', label);
+  }
+);
+
+Then('validates waypoint Info data for {string}', (type: string) => {
+  cy.get('[data-test=waypointfor-title]').should('exist').and('contain', type);
   cy.get('[role="grid"]').should('be.visible').get('[data-label=RDS]').and('contain', 'IGNORED');
 });
 
diff --git a/frontend/cypress/integration/featureFiles/app_details_graph-cy.feature b/frontend/cypress/integration/featureFiles/app_details_graph-cy.feature
deleted file mode 100644
index c7dcc5d33..000000000
--- a/frontend/cypress/integration/featureFiles/app_details_graph-cy.feature
+++ /dev/null
@@ -1,15 +0,0 @@
-@app-details
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali App Details page
-
-  Some App Details minigraph tests, which required a different setup.
-
-  Background:
-    Given user is at administrator perspective
-    And user is at the details page for the "app" "bookinfo/details" located in the "" cluster
-
-  @bookinfo-app
-  Scenario: See app minigraph for details app.
-    Then user sees a cytoscape minigraph
\ No newline at end of file
diff --git a/frontend/cypress/integration/featureFiles/app_details_multicluster_graph-cy.feature b/frontend/cypress/integration/featureFiles/app_details_multicluster_graph-cy.feature
deleted file mode 100644
index 241ecd4d0..000000000
--- a/frontend/cypress/integration/featureFiles/app_details_multicluster_graph-cy.feature
+++ /dev/null
@@ -1,41 +0,0 @@
-@app-details-multi-cluster
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-@multi-cluster
-Feature: Kiali App Details page minigraph in multicluster setup
-
-  Some App Details minigraph tests, which required a different setup.
-  Minigraph should not be displayed for an app, if it is not present in the cluster.
-  We should also be able to navigate to a remote cluster, if an app of one is present on the graph.
-
-  Scenario: See app minigraph for details app.
-    Given user is at administrator perspective
-    And user is at the details page for the "app" "bookinfo/reviews" located in the "west" cluster
-    Then user sees a cytoscape minigraph
-    And user sees "app" from a remote "west" cluster in the cytoscape minigraph
-
-  Scenario: Minigraph should not be visible for app, which is not deployed in specific cluster.
-    Given user is at administrator perspective
-    And user is at the details page for the "app" "bookinfo/details" located in the "west" cluster
-    Then user does not see a cytoscape minigraph
-
-  Scenario Outline: User should be able to navigate through the graph to remotely located apps, services and workloads
-    Given user is at administrator perspective
-    Given user is at the details page for the "app" "bookinfo/productpage" located in the "east" cluster
-    And the "<name>" "<type>" from the "west" cluster is visible in the cytoscape minigraph
-    When user clicks on the "<name>" "<type>" from the "west" cluster in the cytoscape minigraph
-    Then the browser is at the details page for the "<type>" "bookinfo/<name>" located in the "west" cluster
-
-    Examples:
-      | type     | name       |
-      | app      | reviews    |
-      | service  | reviews    |
-      | workload | reviews-v3 |
-
-  # This is a regression test for this bug (https://github.com/kiali/kiali/issues/6185)
-  # This is only multi-primary because that is the suite that has openid setup.
-  @multi-primary
-  Scenario: Remote nodes should be restricted if user does not have access rights to a remote namespace
-    Given user is at limited user perspective
-    When user is at the details page for the "app" "bookinfo/productpage" located in the "east" cluster
-    Then the nodes on the cytoscape minigraph located in the "west" cluster should be restricted
diff --git a/frontend/cypress/integration/featureFiles/graph_context_menu-cy.feature b/frontend/cypress/integration/featureFiles/graph_context_menu-cy.feature
deleted file mode 100644
index 3e4bcd550..000000000
--- a/frontend/cypress/integration/featureFiles/graph_context_menu-cy.feature
+++ /dev/null
@@ -1,86 +0,0 @@
-@graph-context-menu
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Context menu actions
-
-  User opens the Graph page and opens the context menu of graph nodes.
-
-  Background:
-    Given user is at administrator perspective
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-
-  @bookinfo-app
-  Scenario: Actions in context menu for service node with existing traffic routing
-    And user opens the context menu of the "productpage" service node in the cytoscape graph
-    And user should see no cluster parameter in the url when clicking the "Details" link in the context menu in the cytoscape graph
-    And user opens the context menu of the "productpage" service node in the cytoscape graph
-    And user should see no cluster parameter in the url when clicking the "Traffic" link in the context menu in the cytoscape graph
-    And user opens the context menu of the "productpage" service node in the cytoscape graph
-    And user should see no cluster parameter in the url when clicking the "Inbound Metrics" link in the context menu in the cytoscape graph
-    And user opens the context menu of the "productpage" service node in the cytoscape graph
-    And user clicks the "delete-traffic-routing" item of the context menu in the cytoscape graph
-    Then user should see the confirmation dialog to delete all traffic routing
-
-  @bookinfo-app
-  Scenario Outline: Ability to launch <action> wizard from graph context menu
-    And user opens the context menu of the "reviews" service node in the cytoscape graph
-    And user clicks the "<action>" action of the context menu in the cytoscape graph
-    Then user should see the "<action>" wizard
-
-    Examples:
-      | action               |
-      | traffic_shifting     |
-      | tcp_traffic_shifting |
-      | request_routing      |
-      | fault_injection      |
-      | request_timeouts     |
-
-  @multi-cluster
-  Scenario: Actions in context menu for a service node with existing traffic routing
-    And there is traffic routing for the "details" service in the "bookinfo" namespace and in the "east" cluster
-    And user opens the context menu of the "details" service node on the "east" cluster in the cytoscape graph
-    And user should see the "east" cluster parameter in the url when clicking the "Details" link in the context menu in the cytoscape graph
-    And user opens the context menu of the "details" service node on the "east" cluster in the cytoscape graph
-    And user should see the "east" cluster parameter in the url when clicking the "Traffic" link in the context menu in the cytoscape graph
-    And user opens the context menu of the "details" service node on the "east" cluster in the cytoscape graph
-    And user should see the "east" cluster parameter in the url when clicking the "Inbound Metrics" link in the context menu in the cytoscape graph
-    And user opens the context menu of the "details" service node on the "east" cluster in the cytoscape graph
-    And user clicks the "delete-traffic-routing" item of the context menu in the cytoscape graph
-    Then user should see the confirmation dialog to delete all traffic routing
-    When user chooses to delete the routing in the cystoscape graph
-    And user is at the "istio" list page
-    Then user does not see traffic routing objects for the "details" service in the "bookinfo" namespace in the "east" cluster
-
-  @multi-cluster 
-  Scenario Outline: Ability to launch <action> wizard from graph context menu for a remote service node
-    And user opens the context menu of the "ratings" service node on the "west" cluster in the cytoscape graph
-    And user clicks the "<action>" action of the context menu in the cytoscape graph
-    Then user should see the "<action>" wizard
-
-    Examples:
-      | action               |
-      | traffic_shifting     |
-      | tcp_traffic_shifting |
-      | request_routing      |
-      | fault_injection      |
-      | request_timeouts     |
-
-  @multi-primary
-  @multi-cluster 
-  Scenario: Actions in context menu for a remote service node with existing traffic routing
-    And there is no traffic routing for the "ratings" service in the "bookinfo" namespace and in the "west" cluster
-    And there is no traffic routing for the "ratings" service in the "bookinfo" namespace and in the "east" cluster
-    And user opens the context menu of the "ratings" service node on the "west" cluster in the cytoscape graph
-    And user clicks the "request_routing" action of the context menu in the cytoscape graph
-    Then user should see the "request_routing" wizard
-    And user adds a route
-    And user previews the configuration
-    And user creates the configuration
-    # This is a bit of a hack to ensure that traffic stays healthy on the traffic graph.
-    # In multi-primary, istio configuration for cross cluster services needs to
-    # be duplicated across clusters but the wizards only create the istio config
-    # on a single cluster.
-    And configuration is duplicated to the "east" cluster in the cytoscape graph
-    And user is at the "istio" list page
-    Then user sees traffic routing objects for the "ratings" service in the "bookinfo" namespace in the "west" cluster
diff --git a/frontend/cypress/integration/featureFiles/graph_display-cy.feature b/frontend/cypress/integration/featureFiles/graph_display-cy.feature
deleted file mode 100644
index a693986e6..000000000
--- a/frontend/cypress/integration/featureFiles/graph_display-cy.feature
+++ /dev/null
@@ -1,288 +0,0 @@
-@graph-display
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Display menu
-
-  User opens the Graph page and manipulates the "error-rates" demo
-
-  Background:
-    Given user is at administrator perspective
-
-  # NOTE: Graph Find/Hide has its own test script
-  # NOTE: Operation nodes has its own test script
-  # NOTE: Traffic animation, missing sidecars, virtual service, and idle edge options are nominally tested
-
-  @error-rates-app
-  Scenario: Graph no namespaces
-    When user graphs "" namespaces in the cytoscape graph
-    Then user sees no namespace selected
-
-  # gamma will only show nodes when idle-nodes is enabled
-  @error-rates-app
-  Scenario: Graph gamma namespaces
-    When user graphs "gamma" namespaces in the cytoscape graph
-    Then user sees empty graph
-
-  @error-rates-app
-  Scenario: User enables idle nodes
-    When user opens display menu
-    And user "enables" "idle nodes" option
-    Then user sees the "gamma" namespace
-    And idle nodes "appear" in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User disables idle nodes
-    When user "disables" "idle nodes" option
-    Then user sees empty graph
-
-  @error-rates-app
-  Scenario: Graph alpha and beta namespaces
-    When user graphs "alpha,beta" namespaces in the cytoscape graph
-    Then user sees the "alpha" namespace
-    And user sees the "beta" namespace
-
-  @error-rates-app
-  Scenario: User clicks Display Menu
-    When user opens display menu
-    Then the display menu opens
-    And the display menu has default settings
-    And the cytoscape graph reflects default settings
-
-  # percentile variable must match input id
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Average Response-time edge labels
-    When user enables "avg" "responseTime" edge labels
-    Then user sees "responseTime" edge labels in the cytoscape graph
-
-  # percentile variable must match input id
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Median Response-time edge labels
-    When user enables "rt50" "responseTime" edge labels
-    Then user sees "responseTime" edge labels in the cytoscape graph
-
-  # percentile variable must match input id
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: 95th Percentile Response-time edge labels
-    When user enables "rt95" "responseTime" edge labels
-    Then user sees "responseTime" edge labels in the cytoscape graph
-
-  # percentile variable must match input id
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: 99th Percentile Response-time edge labels
-    When user enables "rt99" "responseTime" edge labels
-    Then user sees "responseTime" edge labels in the cytoscape graph
-
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Disable response time edge labels
-    When user "disables" "responseTime" edge labels
-    Then user sees "responseTime" edge label option is closed
-
-  # percentile variable must match input id
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Request Throughput edge labels
-    When user enables "throughputRequest" "throughput" edge labels
-    Then user sees "throughput" edge labels in the cytoscape graph
-
-  # percentile variable must match input id
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Response Throughput edge labels
-    When user enables "throughputResponse" "throughput" edge labels
-    Then user sees "throughput" edge labels in the cytoscape graph
-
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Disable throughput edge labels
-    When user "disables" "throughput" edge labels
-    Then user sees "throughput" edge label option is closed
-
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Enable Traffic Distribution edge labels
-    When user "enables" "trafficDistribution" edge labels
-    Then user sees "trafficDistribution" edge labels in the cytoscape graph
-
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Disable Traffic Distribution edge labels
-    When user "disables" "trafficDistribution" edge labels
-    Then user sees "trafficDistribution" edge label option is closed
-
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Enable Traffic Rate edge labels
-    When user "enables" "trafficRate" edge labels
-    Then user sees "trafficRate" edge labels in the cytoscape graph
-
-  # edge label variable must match edge data name
-  @error-rates-app
-  Scenario: Disable Traffic Rate edge labels
-    When user "disables" "trafficRate" edge labels
-    Then user sees "trafficRate" edge label option is closed
-
-  @error-rates-app
-  Scenario: User disables cluster boxes
-    When user "disables" "cluster boxes" option
-    Then user does not see "Cluster" boxing in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User disables Namespace boxes
-    When user "disables" "namespace boxes" option
-    Then user does not see "Namespace" boxing in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User enables idle edges
-    When user "enables" "idle edges" option
-    Then idle edges "appear" in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User disables idle edges
-    When user "disables" "idle edges" option
-    Then idle edges "do not appear" in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User enables rank
-    When user "enables" "rank" option
-    Then ranks "appear" in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User disables rank
-    When user "disables" "rank" option
-    Then ranks "do not appear" in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User disables service nodes
-    When user "disables" "service nodes" option
-    Then user does not see service nodes in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User enables security
-    When user "enables" "security" option
-    Then security "appears" in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User disables security
-    When user "disables" "security" option
-    Then security "does not appear" in the cytoscape graph
-
-  @error-rates-app
-  Scenario: User disables missing sidecars
-    When user "disables" "missing sidecars" option
-    Then "missing sidecars" option "does not appear" in the graph
-
-  @error-rates-app
-  Scenario: User disables virtual services
-    When user "disables" "virtual services" option
-    Then "virtual services" option "does not appear" in the graph
-
-  @error-rates-app
-  Scenario: User enables animation
-    When user "enables" "traffic animation" option
-    Then "traffic animation" option "appears" in the graph
-
-  @error-rates-app
-  Scenario: User disables animation
-    When user "disables" "traffic animation" option
-    Then "traffic animation" option "does not appear" in the graph
-
-  @error-rates-app
-  Scenario: User resets to factory default setting
-    When user resets to factory default
-    And user opens display menu
-    Then the display menu opens
-    And the display menu has default settings
-
-  @error-rates-app
-  Scenario: User observes some options not being clickable when switching to Service graph
-    When user "disables" "service nodes" option
-    And user "enables" "operation nodes" option
-    And user selects "SERVICE" graph type
-    And user opens display menu
-    Then the display menu opens
-    And the "service nodes" option should "not be checked" and "disabled"
-    And the "operation nodes" option should "be checked" and "disabled"
-    When user selects "APP" graph type
-    And user opens display menu
-    Then the display menu opens
-    And the "service nodes" option should "not be checked" and "enabled"
-    And the "operation nodes" option should "be checked" and "enabled"
-
-  @bookinfo-app
-  Scenario Outline: Multiple cluster boxes should not be visible in the graph
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    And user resets to factory default
-    And user selects "<type>" graph type
-    Then user sees the "bookinfo" namespace
-    And only a single cluster box should be visible in the cytoscape graph
-    Examples:
-      | type          |
-      | APP           |
-      | SERVICE       |
-      | VERSIONED_APP |
-      | WORKLOAD      |
-
-  @multi-cluster
-  Scenario: Graph bookinfo namespace for the multi-cluster setup
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    Then user sees the "bookinfo" namespace
-    And user sees the "bookinfo" namespace deployed across the east and west clusters in the cytoscape graph
-    And nodes in the "east" cluster in the cytoscape graph should contain the cluster name in their links
-    And nodes in the "west" cluster in the cytoscape graph should contain the cluster name in their links
-
-  @multi-cluster
-  Scenario: See link to correct details page after clicking on a node for the east cluster
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    When user clicks on the "productpage-v1" workload in the "bookinfo" namespace in the "east" cluster in the cytoscape graph
-    Then user sees a link to the "east" cluster workload details page in the summary panel
-
-  @multi-cluster
-  Scenario: See link to correct details page after clicking on a node for the west cluster
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    When user clicks on the "reviews-v2" workload in the "bookinfo" namespace in the "west" cluster in the cytoscape graph
-    Then user sees a link to the "west" cluster workload details page in the summary panel
-
-  #inspired by this: https://github.com/kiali/kiali/pull/6469
-  #in order to test this properly, Istio CRDs should be enabled in west and atleast 1 Istio object should be created there
-  @multi-cluster
-  @multi-primary
-  Scenario: See Istio config validations from both clusters
-    Given there are Istio objects in the "bookinfo" namespace for "east" cluster
-    And there are Istio objects in the "bookinfo" namespace for "west" cluster
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    Then the Istio objects for the "bookinfo" namespace for both clusters should be grouped together in the panel
-
-  @multi-cluster
-  Scenario Outline: User double clicks node from specific cluster
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    And user double-clicks on the "reviews" "<type>" from the "<cluster>" cluster in the main cytoscape graph
-    Then the browser is at the details page for the "<type>" "bookinfo/reviews" located in the "<cluster>" cluster
-
-    Examples:
-      | type     | cluster |
-      | app      | east    |
-      | app      | west    |
-      | service  | east    |
-      | service  | west    |
-
-  @ambient
-  Scenario: User sees tcp traffic
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    Then user sees the "bookinfo" namespace
-    Then user opens traffic menu
-    And user "disables" "http" traffic option
-    Then 6 edges appear in the cytoscape graph
-
-  @ambient
-  Scenario: User sees http traffic
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    Then user sees the "bookinfo" namespace
-    Then user opens traffic menu
-    And user "disables" "tcp" traffic option
-    Then 2 edges appear in the cytoscape graph
diff --git a/frontend/cypress/integration/featureFiles/graph_display_user-cy.feature b/frontend/cypress/integration/featureFiles/graph_display_user-cy.feature
deleted file mode 100644
index 5c7d16692..000000000
--- a/frontend/cypress/integration/featureFiles/graph_display_user-cy.feature
+++ /dev/null
@@ -1,18 +0,0 @@
-@graph-display-user
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Display menu for non-admin users
-
-  Non-admin user opens the Graph page and manipulates the graph
-
-  Background:
-    Given user is at limited user perspective
-
-  # This is a regression test for this bug (https://github.com/kiali/kiali/issues/6185)
-  # This is only multi-primary because that is the suite that has openid setup.
-  @multi-cluster
-  @multi-primary
-  Scenario: Remote nodes should be restricted if user does not have access rights to a remote namespace
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    Then the nodes on the cytoscape graph located in the "west" cluster should be restricted
diff --git a/frontend/cypress/integration/featureFiles/graph_find_hide-cy.feature b/frontend/cypress/integration/featureFiles/graph_find_hide-cy.feature
deleted file mode 100644
index e3824bb16..000000000
--- a/frontend/cypress/integration/featureFiles/graph_find_hide-cy.feature
+++ /dev/null
@@ -1,53 +0,0 @@
-@graph-page-find-hide
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Find/Hide
-
-  User opens the Graph page and manipulates the "error-rates" demo.
-  The find/hide feature lets the user either highlight or hide nodes
-  on the graph that match the query expression. Some preset queries
-  allow the user to easily get started using find/hide and a help
-  section describes in more detail how to use find/hide.
-
-  Background:
-    Given user is at administrator perspective
-    And user graphs "alpha,beta" namespaces in the cytoscape graph
-
-  @error-rates-app
-  Scenario: Find unhealthy workloads
-    Then user sees nothing highlighted on the cytoscape graph
-    When user finds unhealthy workloads
-    Then user sees unhealthy workloads highlighted on the cytoscape graph
-
-  @error-rates-app
-  Scenario: Hide unhealthy workloads
-    When user hides unhealthy workloads
-    Then user sees no unhealthy workloads on the cytoscape graph
-
-  @error-rates-app
-  Scenario: Use preset find option to filter workloads
-    Then user sees preset find options
-    When user selects the preset the find option "Find: unhealthy nodes"
-    Then user sees unhealthy workloads highlighted on the cytoscape graph
-
-  @error-rates-app
-  Scenario: Use preset hide option to filter workloads
-    Then user sees preset hide options
-    When user selects the preset hide option "Hide: healthy nodes"
-    Then user sees no healthy workloads on the cytoscape graph
-
-  @error-rates-app
-  Scenario: Show Graph Find/Hide help menu
-    When user seeks help for find and hide
-    Then user sees the help menu
-    And the help menu has info on "Examples"
-    And the help menu has info on "Nodes"
-    And the help menu has info on "Edges"
-    And the help menu has info on "Operators"
-    And the help menu has info on "Usage Notes"
-
-  @error-rates-app
-  Scenario: Filling the find form with nonsense
-    When user fills "hello world" in find and submits
-    Then user sees the "Find: No valid operator found in expression" message
diff --git a/frontend/cypress/integration/featureFiles/graph_replay-cy.feature b/frontend/cypress/integration/featureFiles/graph_replay-cy.feature
deleted file mode 100644
index 755649c8a..000000000
--- a/frontend/cypress/integration/featureFiles/graph_replay-cy.feature
+++ /dev/null
@@ -1,32 +0,0 @@
-@graph-page-replay
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Replay
-
-  User opens the Graph page and replays the "alpha" and "beta" namespace traffic
-
-  Background:
-    Given user is at administrator perspective
-
-  @error-rates-app
-  Scenario: Graph alpha and beta namespaces
-    When user graphs "alpha,beta" namespaces in the cytoscape graph
-    Then user sees the "alpha" namespace
-    And user sees the "beta" namespace
-
-  @error-rates-app
-  Scenario: Show Replay
-    When user presses the Replay button
-    Then user sees the Replay Close button
-    And user presses the Play button
-    And user sees the slider
-    And user presses the "fast" speed button
-    And user presses the "slow" speed button
-    And user presses the "medium" speed button
-    And user presses the Pause button
-
-  @error-rates-app
-  Scenario: Close Replay
-    When user presses the Replay Close button
-    Then user no longer sees the slider
diff --git a/frontend/cypress/integration/featureFiles/graph_side_panel-cy.feature b/frontend/cypress/integration/featureFiles/graph_side_panel-cy.feature
deleted file mode 100644
index de26f69c5..000000000
--- a/frontend/cypress/integration/featureFiles/graph_side_panel-cy.feature
+++ /dev/null
@@ -1,107 +0,0 @@
-@graph-side-panel
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Side panel menu actions
-
-  User opens the Graph page and uses the kebab menu to perform actions.
-
-  Background:
-    Given user is at administrator perspective
-
-  @bookinfo-app
-  Scenario: Actions in kebab menu of the side panel for a service node with existing traffic routing
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    And user clicks the "productpage" "service" node in the cytoscape graph
-    And no cluster badge for the "graph side panel" should be visible
-    And user opens the kebab menu of the graph side panel
-    And user clicks the "delete_traffic_routing" item of the kebab menu of the graph side panel
-    Then user should see the confirmation dialog to delete all traffic routing
-
-  @bookinfo-app
-  Scenario Outline: Ability to launch <action> wizard from graph side panel
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    And user clicks the "reviews" "service" node in the cytoscape graph
-    And no cluster badge for the "graph side panel" should be visible
-    And user opens the kebab menu of the graph side panel
-    When user clicks the "<action>" item of the kebab menu of the graph side panel
-    Then user should see the "<action>" wizard
-
-    Examples:
-      | action               |
-      | traffic_shifting     |
-      | tcp_traffic_shifting |
-      | request_routing      |
-      | fault_injection      |
-      | request_timeouts     |
-
-  @multi-cluster
-  @multi-primary
-  Scenario: Actions in kebab menu of the side panel for a service node with existing traffic routing
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    And there is traffic routing for the "ratings" service in the "bookinfo" namespace and in the "west" cluster
-    And user clicks the "ratings" service node in the "bookinfo" namespace in the "west" cluster in the cytoscape graph
-    And the side panel links should contain a "clusterName=west" parameter
-    And "west" cluster badge for the graph side panel should be visible
-    And user opens the kebab menu of the graph side panel
-    And user clicks the "delete_traffic_routing" item of the kebab menu of the graph side panel
-    Then user should see the confirmation dialog to delete all traffic routing
-    When user chooses to delete the routing in the cystoscape graph
-    And user is at the "istio" list page
-    Then user does not see traffic routing objects for the "ratings" service in the "bookinfo" namespace in the "west" cluster
-
-  @multi-cluster
-  Scenario Outline: Ability to launch <action> wizard from graph side panel
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    And user clicks the "ratings" service node in the "bookinfo" namespace in the "west" cluster in the cytoscape graph
-    And user opens the kebab menu of the graph side panel
-    And user clicks the "<action>" item of the kebab menu of the graph side panel
-    Then user should see the "<action>" wizard
-
-    Examples:
-      | action               |
-      | traffic_shifting     |
-      | tcp_traffic_shifting |
-      | request_routing      |
-      | fault_injection      |
-      | request_timeouts     |
-
-  @multi-cluster
-  @multi-primary
-  Scenario: Actions in context menu for a remote service node with existing traffic routing
-    Given user is at the "istio" list page
-    And there is no traffic routing for the "ratings" service in the "bookinfo" namespace and in the "west" cluster
-    When user graphs "bookinfo" namespaces in the cytoscape graph
-    And user opens the context menu of the "ratings" service node on the "west" cluster in the cytoscape graph
-    And user clicks the "request_routing" action of the context menu in the cytoscape graph
-    Then user should see the "request_routing" wizard
-    When user adds a route rule
-    And user previews the configuration
-    And user creates the configuration
-    And user is at the "istio" list page
-    Then user sees traffic routing objects for the "ratings" service in the "bookinfo" namespace in the "west" cluster
-
-  @multi-cluster
-  Scenario: Show Traces button contains clusterName param
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    And user clicks the "productpage" "service" node in the cytoscape graph
-    And "east" cluster badge for the graph side panel should be visible
-    And user clicks the "Traces" graph summary tab
-    Then user should see "east" cluster parameter in links in the traces
-
-  @bookinfo-app
-  @tracing
-  Scenario: Traces tab contains traces
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    And user clicks the "productpage" "service" node in the cytoscape graph
-    And service badge for the graph side panel should be visible
-    And user clicks the "Traces" graph summary tab
-    Then user should see the traces tab not empty
-
-  @bookinfo-app
-  Scenario: Validate summary panel edge
-    Given user graphs "bookinfo" namespaces in the cytoscape graph
-    And user clicks the edge from "productpage" "app" to "details" "service" in the cytoscape graph
-    And service badge for the graph side panel should be visible
-    And app badge for the graph side panel should be visible
-    And summary panel contains "Edge (HTTP)"
diff --git a/frontend/cypress/integration/featureFiles/graph_toolbar-cy.feature b/frontend/cypress/integration/featureFiles/graph_toolbar-cy.feature
deleted file mode 100644
index 9c6ecd3cc..000000000
--- a/frontend/cypress/integration/featureFiles/graph_toolbar-cy.feature
+++ /dev/null
@@ -1,129 +0,0 @@
-@graph-toolbar
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Toolbar (various)
-
-  User opens the Graph page and manipulates the "error-rates" demo via Toolbar.
-
-  Background:
-    Given user is at administrator perspective
-
-  # NOTE: Graph Display menu has its own test script
-  #       - tests empty graph as well
-  # NOTE: Graph Find/Hide has its own test script
-  # NOTE: Graph Replay has its own test script
-
-  Scenario: On the graph page the namespace selector should be sorted alphabetically
-    When user graphs "" namespaces in the cytoscape graph
-    Then the namespace dropdown is sorted alphabetically
-
-  @error-rates-app
-  Scenario: Graph alpha namespace with query params in the cytoscape graph
-    When user graphs "alpha" namespaces with refresh "900000" and duration "300" in the cytoscape graph
-    Then user sees the "alpha" namespace
-    And user sees selected graph duration "Last 5m"
-    And user sees selected graph refresh "Every 15m"
-
-  @error-rates-app
-  Scenario: Open graph Tour
-    When user clicks graph tour
-    Then user "sees" graph tour
-
-  @error-rates-app
-  Scenario: Close graph Tour
-    When user closes graph tour
-    Then user "does not see" graph tour
-
-  @error-rates-app
-  Scenario: Open traffic dropdown
-    When user clicks graph traffic menu
-    Then user sees "default" graph traffic menu
-
-  @error-rates-app
-  Scenario: Disable all traffic
-    When user disables all traffic
-    Then user does not see any traffic
-
-  # todo: would be a better test if demos has tcp and/or grpc traffic
-  @error-rates-app
-  Scenario: Enable http traffic
-    When user disables all traffic
-    When user enables "http" traffic
-    Then user "sees" "http" traffic in the cytoscape graph
-    And user "does not see" "tcp" traffic in the cytoscape graph
-    And user "does not see" "grpc" traffic in the cytoscape graph
-
-  @error-rates-app
-  Scenario: Close traffic dropdown
-    When user clicks graph traffic menu
-    Then user does not see graph traffic menu
-
-  @error-rates-app
-  @graph-page-display
-  Scenario: User resets to factory default
-    When user resets to factory default
-    And user clicks graph traffic menu
-    Then user sees "default" graph traffic menu
-
-  @error-rates-app
-  Scenario: Open duration dropdown
-    When user clicks graph duration menu
-    Then user sees graph duration menu
-
-  @error-rates-app
-  Scenario: Close duration dropdown
-    When user clicks graph duration menu
-    Then user does not see graph duration menu
-
-  @error-rates-app
-  Scenario: Set duration dropdown
-    When user selects graph duration "600"
-    Then user sees selected graph duration "Last 10m"
-
-  @error-rates-app
-  Scenario: Open refresh dropdown
-    When user clicks graph refresh menu
-    Then user sees graph refresh menu
-
-  @error-rates-app
-  Scenario: Close refresh dropdown
-    When user clicks graph refresh menu
-    Then user does not see graph refresh menu
-
-  @error-rates-app
-  Scenario: Set refresh dropdown
-    When user selects graph refresh "0"
-    Then user sees selected graph refresh "Pause"
-
-  @error-rates-app
-  Scenario: graph type app
-    When user selects "APP" graph type
-    Then user sees a "app" cytoscape graph
-
-  @error-rates-app
-  Scenario: graph type service
-    When user selects "SERVICE" graph type
-    Then user sees a "service" cytoscape graph
-
-  @error-rates-app
-  Scenario: graph type versioned app
-    When user selects "VERSIONED_APP" graph type
-    Then user sees a "versionedApp" cytoscape graph
-
-  @error-rates-app
-  Scenario: graph type workload
-    When user selects "WORKLOAD" graph type
-    Then user sees a "workload" cytoscape graph
-
-  @ambient
-  Scenario: Open traffic dropdown for ambient
-    When user graphs "" namespaces in the cytoscape graph
-    And user clicks graph traffic menu
-    Then user sees "ambient" graph traffic menu
-
-  @ambient
-  Scenario: Close traffic dropdown for ambient
-    When user clicks graph traffic menu
-    Then user does not see graph traffic menu
-
diff --git a/frontend/cypress/integration/featureFiles/graph_toolbar_legend-cy.feature b/frontend/cypress/integration/featureFiles/graph_toolbar_legend-cy.feature
deleted file mode 100644
index 1199410ea..000000000
--- a/frontend/cypress/integration/featureFiles/graph_toolbar_legend-cy.feature
+++ /dev/null
@@ -1,147 +0,0 @@
-@graph-toolbar-legend
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Graph page - Graph toolbar and legend sidebar
-
-  User opens the Graph page and manipulates the "alpha", "beta" namespace graph with buttons
-  located at the bottom of the page
-
-  Background:
-    Given user is at administrator perspective
-    And user graphs "alpha,beta" namespaces in the cytoscape graph
-
-  @error-rates-app
-  Scenario Outline: Check if the <label> button is usable
-    Then the toggle button "<label>" is enabled in the cytoscape graph
-    Examples:
-      | label                      |
-      | Toggle Drag                |
-      | Zoom to Fit                |
-      | Hide Healthy Edges         |
-      | Hide All Edges             |
-      | Graph Layout Default Style |
-      | Graph Layout Style 1       |
-      | Graph Layout Style 2       |
-      | Graph Layout Style 3       |
-      | Namespace Layout Style 1   |
-      | Namespace Layout Style 2   |
-      | Show Legend                |
-
-  @error-rates-app
-  Scenario: Check if the Toggle Drag Graph button can be turned off
-    When the button "Toggle Drag" is clicked in the cytoscape graph
-    Then the button "Toggle Drag" is not active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: Check if the Toggle Drag Graph button can be turned on
-    When the "Toggle Drag" is turned off in the cytoscape graph
-    And the button "Toggle Drag" is clicked in the cytoscape graph
-    Then the button "Toggle Drag" is active in the cytoscape graph
-
-  @error-rates-app
-  Scenario Outline: Check if the not active by default <label> Graph button can be turned on
-    When the button "<label>" is clicked in the cytoscape graph
-    Then the button "<label>" is active in the cytoscape graph
-    Examples:
-      | label                    |
-      | Hide Healthy Edges       |
-      | Hide All Edges           |
-      | Graph Layout Style 1     |
-      | Graph Layout Style 2     |
-      | Graph Layout Style 3     |
-      | Namespace Layout Style 2 |
-
-  @error-rates-app
-  Scenario Outline: Check if the not active by default <label> Graph button can be turned off
-    When the "<label>" is turned on in the cytoscape graph
-    And the button "<label>" is clicked in the cytoscape graph
-    Then the button "<label>" is not active in the cytoscape graph
-    Examples:
-      | label              |
-      | Hide Healthy Edges |
-      | Hide All Edges     |
-
-  @error-rates-app
-  Scenario: The Hide Healthy Edges is turned off by turning on the Hide All Edges
-    When the "Hide Healthy Edges" is turned off in the cytoscape graph
-    And the "Hide All Edges" is turned off in the cytoscape graph
-    And the button "Hide Healthy Edges" is clicked in the cytoscape graph
-    And the button "Hide All Edges" is clicked in the cytoscape graph
-    Then the button "Hide Healthy Edges" is not active in the cytoscape graph
-    And the button "Hide All Edges" is active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: The Hide All Edges is turned off by turning on the Hide Healthy Edges
-    When the "Hide Healthy Edges" is turned off in the cytoscape graph
-    And the "Hide All Edges" is turned off in the cytoscape graph
-    And the button "Hide All Edges" is clicked in the cytoscape graph
-    And the button "Hide Healthy Edges" is clicked in the cytoscape graph
-    Then the button "Hide Healthy Edges" is active in the cytoscape graph
-    And the button "Hide All Edges" is not active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: The Namespace Layout Style 1 is turned off by turning on the Namespace Layout Style 2
-    When the "Namespace Layout Style 1" is turned off in the cytoscape graph
-    And the "Namespace Layout Style 2" is turned off in the cytoscape graph
-    And the button "Namespace Layout Style 1" is clicked in the cytoscape graph
-    And the button "Namespace Layout Style 2" is clicked in the cytoscape graph
-    Then the button "Namespace Layout Style 1" is not active in the cytoscape graph
-    And the button "Namespace Layout Style 2" is active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: The Namespace Layout Style 2 is turned off by turning on the Namespace Layout Style 1
-    When the "Namespace Layout Style 1" is turned off in the cytoscape graph
-    And the "Namespace Layout Style 2" is turned off in the cytoscape graph
-    And the button "Namespace Layout Style 2" is clicked in the cytoscape graph
-    And the button "Namespace Layout Style 1" is clicked in the cytoscape graph
-    Then the button "Namespace Layout Style 1" is active in the cytoscape graph
-    And the button "Namespace Layout Style 2" is not active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: First 3 Graph Layout Style buttons are mutually exclusive
-    When the "Graph Layout Default Style" is turned on in the cytoscape graph
-    And the "Graph Layout Style 1" is turned off in the cytoscape graph
-    And the "Graph Layout Style 2" is turned off in the cytoscape graph
-    And the "Graph Layout Style 3" is turned off in the cytoscape graph
-    And the button "Graph Layout Style 1" is clicked in the cytoscape graph
-    And the button "Graph Layout Style 2" is clicked in the cytoscape graph
-    And the button "Graph Layout Style 3" is clicked in the cytoscape graph
-    Then the button "Graph Layout Default Style" is not active in the cytoscape graph
-    And the button "Graph Layout Style 1" is not active in the cytoscape graph
-    And the button "Graph Layout Style 2" is not active in the cytoscape graph
-    And the button "Graph Layout Style 3" is active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: The last Graph Layout Style button is mutually exclusive with the rest
-    When the "Graph Layout Default Style" is turned off in the cytoscape graph
-    And the "Graph Layout Style 1" is turned off in the cytoscape graph
-    And the "Graph Layout Style 2" is turned off in the cytoscape graph
-    And the "Graph Layout Style 3" is turned on in the cytoscape graph
-    And the button "Graph Layout Style 2" is clicked in the cytoscape graph
-    And the button "Graph Layout Style 1" is clicked in the cytoscape graph
-    And the button "Graph Layout Default Style" is clicked in the cytoscape graph
-    Then the button "Graph Layout Default Style" is active in the cytoscape graph
-    And the button "Graph Layout Style 1" is not active in the cytoscape graph
-    And the button "Graph Layout Style 2" is not active in the cytoscape graph
-    And the button "Graph Layout Style 3" is not active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: Show the Legend
-    When the button "Show Legend" is clicked in the cytoscape graph
-    Then user can see the legend section
-    And the button "Show Legend" is active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: Close the Legend using the button
-    When the Legend section is visible in the cytoscape graph
-    And the button "Show Legend" is clicked in the cytoscape graph
-    Then user cannot see the legend section
-    And the button "Show Legend" is not active in the cytoscape graph
-
-  @error-rates-app
-  Scenario: Close the Legend using the cross
-    When the Legend section is visible in the cytoscape graph
-    And the cross is clicked
-    Then user cannot see the legend section
-    And the button "Show Legend" is not active in the cytoscape graph
diff --git a/frontend/cypress/integration/featureFiles/kiali_login.feature b/frontend/cypress/integration/featureFiles/kiali_login.feature
index 597218a9d..b1ed16648 100644
--- a/frontend/cypress/integration/featureFiles/kiali_login.feature
+++ b/frontend/cypress/integration/featureFiles/kiali_login.feature
@@ -5,13 +5,6 @@ Feature: Kiali login
 
   User wants to login to Kiali and see landing page
 
-  Scenario: Try to log in without filling the username and password
-    Given all sessions are cleared
-    And user opens base url
-    And user clicks my_htpasswd_provider
-    And user does not fill in username and password
-    Then user sees the "Login is required. Please try again." phrase displayed
-
   Scenario: Try to log in with an invalid username
     Given all sessions are cleared
     And user opens base url
diff --git a/frontend/cypress/integration/featureFiles/service_details_graph-cy.feature b/frontend/cypress/integration/featureFiles/service_details_graph-cy.feature
deleted file mode 100644
index eba8b0f95..000000000
--- a/frontend/cypress/integration/featureFiles/service_details_graph-cy.feature
+++ /dev/null
@@ -1,22 +0,0 @@
-@service-details
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Service Details page
-
-  User opens the Services page and sees the bookinfo namespaces,
-  clicks in the productpage service, and page loads correctly.
-
-  Background:
-    Given user is at administrator perspective
-    And user is at the details page for the "service" "bookinfo/productpage" located in the "" cluster
-
-  @bookinfo-app
-  Scenario: See service minigraph for details app.
-    Then user sees a cytoscape minigraph
-
-  @bookinfo-app
-  Scenario: Verify that the Graph type dropdown is disabled when changing to Show node graph
-    When user sees a cytoscape minigraph
-    And user chooses the "Show node graph" option
-    Then the graph type is disabled
diff --git a/frontend/cypress/integration/featureFiles/service_details_multicluster_graph-cy.feature b/frontend/cypress/integration/featureFiles/service_details_multicluster_graph-cy.feature
deleted file mode 100644
index cc155bc54..000000000
--- a/frontend/cypress/integration/featureFiles/service_details_multicluster_graph-cy.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-@multi-cluster
-@service-details-multi-cluster
-@cytoscape
-Feature: Kiali Service Details page for remote cluster
-
-  User opens the Services page and sees the bookinfo namespaces,
-  clicks in the productpage service, and page loads correctly.
-
-  Background:
-    Given user is at administrator perspective
-    And user is at the details page for the "service" "bookinfo/ratings" located in the "west" cluster
-
-  Scenario: See service minigraph for details app.
-    Then user sees a cytoscape minigraph
-    And user sees "service" from a remote "west" cluster in the cytoscape minigraph
-
-  Scenario: Minigraph should not be visible for a service, which is not deployed in specific cluster.
-    And user is at the details page for the "service" "bookinfo/details" located in the "west" cluster
-    Then user does not see a cytoscape minigraph
diff --git a/frontend/cypress/integration/featureFiles/waypoint.feature b/frontend/cypress/integration/featureFiles/waypoint.feature
index e3756e5d0..0d4be8839 100644
--- a/frontend/cypress/integration/featureFiles/waypoint.feature
+++ b/frontend/cypress/integration/featureFiles/waypoint.feature
@@ -50,7 +50,7 @@ Feature: Kiali Waypoint related features
     Then user goes to the waypoint "Services" subtab
     And validates Services data
     Then user goes to the waypoint "Info" subtab
-    And validates waypoint Info data
+    And validates waypoint Info data for "service"
 
     Scenario: [Workload details - ztunnel] The workload details for a ztunnel are valid
     Given user is at the details page for the "workload" "istio-system/ztunnel" located in the "" cluster
@@ -113,3 +113,256 @@ Feature: Kiali Waypoint related features
     When user clicks in the "LIST" view
     Then user sees a "LIST" "bookinfo" namespace
     And badge for "istio.io/use-waypoint=waypoint" is visible in the LIST view in the namespace "bookinfo"
+
+  Scenario: [Traffic] Waypoint for different namespaces working as expected
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-differentns" namespaces
+    Then user sees the "waypoint-differentns" namespace
+    Then user opens traffic menu
+    And user "enables" "ambient" traffic option
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "disables" "waypoint proxies" option
+    Then 2 edges appear in the graph
+    And the "echo-server" node "does" exists
+    And the "curl-client" node "does" exists
+
+  Scenario: [Traffic] Waypoint for different namespaces working as expected with waypoints
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-differentns" namespaces
+    Then user sees the "waypoint-differentns" namespace
+    Then user opens traffic menu
+    And user "enables" "ambient" traffic option
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "enables" "waypoint proxies" option
+    Then 4 edges appear in the graph
+    Then user opens traffic menu
+    And user "disables" "http" traffic option
+    Then 2 edges appear in the graph
+
+  Scenario: [Traffic] Waypoint for all
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-forall" namespaces
+    Then user sees the "waypoint-forall" namespace
+    Then user opens traffic menu
+    And user "enables" "ambient" traffic option
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "disables" "waypoint proxies" option
+    Then 2 edges appear in the graph
+    And the "echo-server" node "does" exists
+    And the "curl-client" node "does" exists
+
+  Scenario: [Traffic] Waypoint for all with waypoint
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-forall" namespaces
+    Then user sees the "waypoint-forall" namespace
+    Then user opens traffic menu
+    And user "enables" "ambient" traffic option
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "enables" "waypoint proxies" option
+    Then 4 edges appear in the graph
+    And the "cgw" node "does" exists
+    Then user opens traffic menu
+    And user "disables" "http" traffic option
+    Then 2 edges appear in the graph
+
+  Scenario: [Traffic] Waypoint for none
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-fornone" namespaces
+    Then user sees the "waypoint-fornone" namespace
+    Then user opens traffic menu
+    And user "enables" "ambient" traffic option
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "disables" "waypoint proxies" option
+    Then 2 edges appear in the graph
+    And the "echo-server" node "does" exists
+    And the "curl-client" node "does" exists
+
+  Scenario: [Traffic] Waypoint for none with waypoint proxies
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-fornone" namespaces
+    Then user sees the "waypoint-fornone" namespace
+    Then user opens traffic menu
+    And user "enables" "ambient" traffic option
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "enables" "waypoint proxies" option
+    Then 2 edges appear in the graph
+    Then user opens traffic menu
+    And user "disables" "http" traffic option
+    Then 0 edges appear in the graph
+
+  Scenario: [Traffic] Waypoint for service
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-forservice" namespaces
+    Then user sees the "waypoint-forservice" namespace
+    Then user opens traffic menu
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "disables" "waypoint proxies" option
+    Then 2 edges appear in the graph
+    And the "echo-server" node "does" exists
+    And the "curl-client" node "does" exists
+
+  Scenario: [Traffic] Waypoint for service with waypoints
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-forservice" namespaces
+    Then user sees the "waypoint-forservice" namespace
+    Then user opens traffic menu
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "enables" "waypoint proxies" option
+    Then 4 edges appear in the graph
+    And the "waypoint" node "does" exists
+    Then user opens traffic menu
+    And user "disables" "http" traffic option
+    Then 2 edges appear in the graph
+
+  @skip-istio-1-23
+  Scenario: [Traffic] Waypoint for workload
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-forworkload" namespaces
+    Then user sees the "waypoint-forworkload" namespace
+    Then user opens traffic menu
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "disables" "waypoint proxies" option
+    Then 1 edges appear in the graph
+    And the "unknown" service "does" exists
+    And the "curl-client" node "does" exists
+
+  @skip-istio-1-23
+  Scenario: [Traffic] Waypoint for workload with waypoints
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-forworkload" namespaces
+    Then user sees the "waypoint-forworkload" namespace
+    Then user opens traffic menu
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "enables" "waypoint proxies" option
+    Then 3 edges appear in the graph
+    And the "bwaypoint" node "does" exists
+    Then user opens traffic menu
+    And user "disables" "http" traffic option
+    Then 2 edges appear in the graph
+
+  Scenario: [Traffic] Waypoint override
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-override" namespaces
+    Then user sees the "waypoint-override" namespace
+    Then user opens traffic menu
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "disables" "waypoint proxies" option
+    Then 2 edges appear in the graph
+    And the "echo-server" node "does" exists
+    And the "curl-client" node "does" exists
+
+  Scenario: [Traffic] Waypoint override with waypoints
+    Given user is at the "graphpf" page
+    When user graphs "waypoint-override" namespaces
+    Then user sees the "waypoint-override" namespace
+    Then user opens traffic menu
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "enables" "waypoint proxies" option
+    Then 4 edges appear in the graph
+    And the "use-this" node "does" exists
+    Then user opens traffic menu
+    And user "disables" "http" traffic option
+    Then 2 edges appear in the graph
+
+  Scenario: [Waypoint details] The waypoint details for a waypoint in different ns are valid
+    Given user is at the details page for the "workload" "waypoint-differentns/curl-client" located in the "" cluster
+    And the user sees the L7 "egress-gateway" link
+    And the link for the waypoint "egress-gateway" should redirect to a valid workload details
+    When the user goes to the "Waypoint" tab
+    Then user goes to the waypoint "Services" subtab
+    And validates Services data with "1" rows and "echo-service" workload, "waypoint-differentns" namespace, "namespace" label for, "pfbadge-S" badge
+    Then user goes to the waypoint "Info" subtab
+    And validates waypoint Info data for "service"
+
+  Scenario: [Waypoint details] The waypoint details for a waypoint for all are valid
+    Given user is at the details page for the "workload" "waypoint-forall/curl-client" located in the "" cluster
+    And the user sees the L7 "cgw" link
+    And the link for the waypoint "cgw" should redirect to a valid workload details
+    When the user goes to the "Waypoint" tab
+    Then user goes to the waypoint "Services" subtab
+    And validates Services data with "1" rows and "echo-service" workload, "waypoint-forall" namespace, "namespace" label for, "pfbadge-S" badge
+    Then user goes to the waypoint "Workloads" subtab
+    And validates Services data with "2" rows and "echo-server" workload, "waypoint-forall" namespace, "namespace" label for, "pfbadge-W" badge
+    Then user goes to the waypoint "Info" subtab
+    And validates waypoint Info data for "all"
+
+  Scenario: [Waypoint details] The waypoint details for a waypoint for none are valid
+    Given user is at the details page for the "workload" "waypoint-fornone/curl-client" located in the "" cluster
+    And the user doesn't see a L7 link
+    And user is at the details page for the "workload" "waypoint-fornone/waypoint" located in the "" cluster
+    When the user goes to the "Waypoint" tab
+    Then the "Services" subtab doesn't exist
+    Then the "Workloads" subtab doesn't exist
+    Then user goes to the waypoint "Info" subtab
+    And validates waypoint Info data for "none"
+
+  Scenario: [Waypoint details] The waypoint details for a waypoint for service are valid
+    Given user is at the details page for the "workload" "waypoint-forservice/curl-client" located in the "" cluster
+    And the user sees the L7 "waypoint" link
+    And the link for the waypoint "waypoint" should redirect to a valid workload details
+    When the user goes to the "Waypoint" tab
+    Then user goes to the waypoint "Services" subtab
+    And validates Services data with "1" rows and "echo-service" workload, "waypoint-forservice" namespace, "namespace" label for, "pfbadge-S" badge
+    Then user goes to the waypoint "Info" subtab
+    And validates waypoint Info data for "service"
+
+  Scenario: [Waypoint details] The waypoint details for a waypoint for workload are valid
+    Given user is at the details page for the "workload" "waypoint-forworkload/echo-server" located in the "" cluster
+    And the user sees the L7 "bwaypoint" link
+    And the link for the waypoint "waypoint" should redirect to a valid workload details
+    When the user goes to the "Waypoint" tab
+    Then user goes to the waypoint "Workloads" subtab
+    And validates Services data with "1" rows and "echo-server" workload, "waypoint-forworkload" namespace, "workload" label for, "pfbadge-W" badge
+    Then user goes to the waypoint "Info" subtab
+    And validates waypoint Info data for "workload"
+
+  Scenario: [Waypoint details] The waypoint details for a waypoint override are valid
+  # TODO: This shouldn't be right
+    Given user is at the details page for the "workload" "waypoint-override/curl-client" located in the "" cluster
+    And the user sees the L7 "waypoint" link
+    And the link for the waypoint "waypoint" should redirect to a valid workload details
+    When the user goes to the "Waypoint" tab
+    Then user goes to the waypoint "Services" subtab
+    And validates Services data with "1" rows and "echo-service" workload, "waypoint-override" namespace, "namespace" label for, "pfbadge-S" badge
+    Then user goes to the waypoint "Info" subtab
+    And validates waypoint Info data for "service"
+  # TODO: End-Todo
+    Then user is at the details page for the "workload" "waypoint-override/echo-server" located in the "" cluster
+    And the user sees the L7 "use-this" link
+    And the link for the waypoint "use-this" should redirect to a valid workload details
+    When the user goes to the "Waypoint" tab
+    Then user goes to the waypoint "Services" subtab
+    And validates Services data with "1" rows and "echo-service" workload, "waypoint-override" namespace, "service" label for, "pfbadge-S" badge
+    Then user goes to the waypoint "Info" subtab
+    And validates waypoint Info data for "service"
+
+  @skip-istio-1-23
+  Scenario: [Traffic] Sidecar Ambient traffic
+    Given user is at the "graphpf" page
+    When user graphs "test-ambient,test-sidecar" namespaces
+    Then user sees the "test-ambient" namespace
+    Then user sees the "test-sidecar" namespace
+    Then user opens traffic menu
+    And user "enables" "http" traffic option
+    Then user opens display menu
+    And user "enables" "security" option
+    Then 9 edges appear in the graph
+    Then security "appears" in the graph
+    Then user opens traffic menu
+    And user "disables" "ambient" traffic option
+    Then 5 edges appear in the graph
+    Then user "enables" "ambient" traffic option
+    Then user "disables" "tcp" traffic option
+    Then 4 edges appear in the graph
diff --git a/frontend/cypress/integration/featureFiles/workloads_details _graph-cy.feature b/frontend/cypress/integration/featureFiles/workloads_details _graph-cy.feature
deleted file mode 100644
index ecd6daf4e..000000000
--- a/frontend/cypress/integration/featureFiles/workloads_details _graph-cy.feature	
+++ /dev/null
@@ -1,17 +0,0 @@
-@workload-details
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-Feature: Kiali Workload Details page
-
-  On the Workload Details page, the user should see the details of a workload as well as
-  a minigraph for traffic going to and originating from the workload. In addition,
-  there should be tabs for viewing workload specific traffic, inbound/outbound metrics, traces and Envoy information, including metrics.
-
-  Background:
-    Given user is at administrator perspective
-    And user is at the details page for the "workload" "bookinfo/details-v1" located in the "" cluster
-
-  @bookinfo-app
-  Scenario: See minigraph for workload.
-    Then user sees a cytoscape minigraph
\ No newline at end of file
diff --git a/frontend/cypress/integration/featureFiles/workloads_details_multicluster_graph-cy.feature b/frontend/cypress/integration/featureFiles/workloads_details_multicluster_graph-cy.feature
deleted file mode 100644
index 3502c6aad..000000000
--- a/frontend/cypress/integration/featureFiles/workloads_details_multicluster_graph-cy.feature
+++ /dev/null
@@ -1,37 +0,0 @@
-@workloads-details-multi-cluster-graph
-@cytoscape
-# don't change first line of this file - the tag is used for the test scripts to identify the test suite
-
-@multi-cluster
-Feature: Kiali Workloads Details minigraph in multicluster setup
-
-  Scenario: See minigraph for workload.
-    Given user is at administrator perspective
-    And user is at the details page for the "workload" "bookinfo/reviews-v2" located in the "west" cluster
-    Then user sees a cytoscape minigraph
-    And user sees "service" from a remote "west" cluster in the cytoscape minigraph
-
-  Scenario: Minigraph should not be visible for a service, which is not deployed in specific cluster.
-    Given user is at administrator perspective
-    And user is at the details page for the "workload" "bookinfo/details-v1" located in the "west" cluster
-    Then user does not see a cytoscape minigraph
-
-  Scenario Outline: User should be able to navigate through the graph to remote workloads and services.
-    Given user is at administrator perspective
-    Given user is at the details page for the "workload" "bookinfo/productpage-v1" located in the "east" cluster
-    And the "<name>" "<type>" from the "west" cluster is visible in the cytoscape minigraph
-    When user clicks on the "<name>" "<type>" from the "west" cluster in the cytoscape minigraph
-    Then the browser is at the details page for the "<type>" "bookinfo/<name>" located in the "west" cluster
-
-    Examples:
-      | type     | name       |
-      | service  | reviews    |
-      | workload | reviews-v3 |
-
-  # This is a regression test for this bug (https://github.com/kiali/kiali/issues/6185)
-  # This is only multi-primary because that is the suite that has openid setup.
-  @multi-primary
-  Scenario: Remote nodes should be restricted if user does not have access rights to a remote namespace
-    Given user is at limited user perspective
-    When user is at the details page for the "workload" "bookinfo/productpage-v1" located in the "east" cluster
-    Then the nodes on the cytoscape minigraph located in the "west" cluster should be restricted
diff --git a/frontend/package.json b/frontend/package.json
index cf764dd2e..ac61ca264 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -46,16 +46,17 @@
     "cypress:multi-cluster": "cypress open -e TAGS=\"@multi-cluster and not @multi-primary\"",
     "cypress:multi-primary": "cypress open -e TAGS=\"@multi-cluster and @multi-primary\"",
     "cypress:perf": "cypress open --config-file cypress-perf.config.ts",
-    "cypress:run": "cypress run -e TAGS=\"@smoke\" && cypress run -e TAGS=\"not @crd-validation and not @multi-cluster and not @smoke and not @ambient and not @waypoint and not @cytoscape and (not @waypoint-tracing or @tracing)\" && cypress run -e TAGS=\"@crd-validation\"",
-    "cypress:run:ambient": "cypress run -e TAGS=\"@ambient and not @cytoscape\" && cypress run -e TAGS=\"@waypoint\" && cypress run -e TAGS=\"@waypoint-tracing\"",
-    "cypress:run:junit": "cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"not @crd-validation and not @multi-cluster and not @ambient and not @waypoint and not @cytoscape and (not @waypoint-tracing or @tracing)\" && cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"@crd-validation\"",
-    "cypress:run:multi-cluster": "cypress run -e TAGS=\"@multi-cluster and not @multi-primary and not @cytoscape\"",
-    "cypress:run:multi-primary": "cypress run -e TAGS=\"@multi-cluster and @multi-primary and not @cytoscape\"",
+    "cypress:run": "cypress run -e TAGS=\"@smoke\" && cypress run -e TAGS=\"not @crd-validation and not @multi-cluster and not @smoke and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\" && cypress run -e TAGS=\"@crd-validation\"",
+    "cypress:run:ambient": "cypress run -e TAGS=\"@ambient\" && cypress run -e TAGS=\"@waypoint\" && cypress run -e TAGS=\"@waypoint-tracing\"",
+    "cypress:run:ambient123": "cypress run -e TAGS=\"@ambient\" && cypress run -e TAGS=\"@waypoint and not @skip-istio-1-23\" && cypress run -e TAGS=\"@waypoint-tracing\"",
+    "cypress:run:junit": "cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"not @crd-validation and not @multi-cluster and not @ambient and not @waypoint and (not @waypoint-tracing or @tracing)\" && cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"@crd-validation\"",
+    "cypress:run:multi-cluster": "cypress run -e TAGS=\"@multi-cluster and not @multi-primary\"",
+    "cypress:run:multi-primary": "cypress run -e TAGS=\"@multi-cluster and @multi-primary\"",
     "cypress:run:perf": "cypress run --config-file cypress-perf.config.ts",
     "cypress:run:selected": "cypress run -e TAGS=\"@selected\"",
     "cypress:run:smoke": "cypress run -e TAGS=\"@smoke\"",
     "cypress:run:test-group:junit": "cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json -e TAGS=\"${TEST_GROUP}\"",
-    "cypress:run:tracing": "cypress run -e TAGS=\"@tracing and not @cytoscape\"",
+    "cypress:run:tracing": "cypress run -e TAGS=\"@tracing\"",
     "cypress:selected": "cypress open -e TAGS=\"@selected\"",
     "cypress:tracing": "cypress open -e TAGS=\"@tracing\"",
     "format:precommit": "../hack/hooks/pre-commit.sh",
@@ -81,10 +82,6 @@
     "@patternfly/react-topology": "^5.4.1",
     "axios": "^1.7.4",
     "bootstrap-slider-without-jquery": "10.0.0",
-    "cy-node-html-label": "2.0.0",
-    "cytoscape": "3.15.5",
-    "cytoscape-canvas": "3.0.1",
-    "cytoscape-popper": "1.0.7",
     "dagre": "0.8.5",
     "deep-freeze": "0.0.1",
     "eventemitter3": "4.0.7",
@@ -124,7 +121,6 @@
     "@badeball/cypress-cucumber-preprocessor": "^20.1.2",
     "@bahmutov/cypress-esbuild-preprocessor": "^2.2.0",
     "@types/cypress-cucumber-preprocessor": "^4.0.5",
-    "@types/cytoscape": "3.14.0",
     "@types/deep-freeze": "^0.1.5",
     "@types/enzyme": "3.10.5",
     "@types/jest": "23.3.10",
diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json
index 5b42c60a9..87850143a 100644
--- a/frontend/public/locales/zh/translation.json
+++ b/frontend/public/locales/zh/translation.json
@@ -228,7 +228,6 @@
   "Legend": "图例",
   "List of enrolled": "List of enrolled",
   "List view": "列表视图",
-  "Loading actions...": "Loading actions...",
   "Loading...": "加载中...",
   "Login to {{cluster}}": "Login to {{cluster}}",
   "Logout": "Logout",
@@ -421,13 +420,10 @@
   "Traffic": "流量",
   "Traffic Animation": "Traffic Animation",
   "Traffic Graph": "图",
-  "Traffic Graph [Cy]": "有向图",
-  "Traffic Graph [PF]": "无向图",
   "Traffic metrics per frame": "每个数据帧的流量指标",
   "Traffic metrics per refresh": "每次刷新时的流量指标",
   "Traffic Policy": "流量策略",
   "Traffic routing already exists for this service": "此服务已存在流量路由规则",
-  "Traffic routing doesn't exist for this service": "Traffic routing doesn't exist for this service",
   "Traffic routing doesn't exists for this service": "该服务的流量路由规则不存在",
   "Traffic Shifting": "流量调整",
   "Traffic Shifting / TCP Traffic Shifting": "Traffic Shifting / TCP Traffic Shifting",
diff --git a/frontend/src/actions/GraphActions.ts b/frontend/src/actions/GraphActions.ts
index dac868eba..fe5526db4 100644
--- a/frontend/src/actions/GraphActions.ts
+++ b/frontend/src/actions/GraphActions.ts
@@ -1,14 +1,15 @@
 import { ActionType, createAction, createStandardAction } from 'typesafe-actions';
-import { GraphEvent, EdgeMode, GraphDefinition, Layout, NodeParamsType, RankResult } from '../types/Graph';
+import { GraphEvent, EdgeMode, GraphDefinition, NodeParamsType, RankResult } from '../types/Graph';
 import { ActionKeys } from './ActionKeys';
 import { TimeInMilliseconds } from 'types/Common';
+import { GraphLayout } from 'pages/Graph/GraphPF';
 
 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>(),
-  setLayout: createStandardAction(ActionKeys.GRAPH_SET_LAYOUT)<Layout>(),
-  setNamespaceLayout: createStandardAction(ActionKeys.GRAPH_SET_NAMESPACE_LAYOUT)<Layout>(),
+  setLayout: createStandardAction(ActionKeys.GRAPH_SET_LAYOUT)<GraphLayout>(),
+  setNamespaceLayout: createStandardAction(ActionKeys.GRAPH_SET_NAMESPACE_LAYOUT)<GraphLayout>(),
   setNode: createStandardAction(ActionKeys.GRAPH_SET_NODE)<NodeParamsType | undefined>(),
   setRankResult: createStandardAction(ActionKeys.GRAPH_SET_RANK_RESULT)<RankResult>(),
   setUpdateTime: createStandardAction(ActionKeys.GRAPH_SET_UPDATE_TIME)<TimeInMilliseconds>(),
diff --git a/frontend/src/actions/GraphThunkActions.ts b/frontend/src/actions/GraphThunkActions.ts
index f0a63a17f..5939d4065 100644
--- a/frontend/src/actions/GraphThunkActions.ts
+++ b/frontend/src/actions/GraphThunkActions.ts
@@ -3,21 +3,10 @@ import { KialiDispatch } from '../types/Redux';
 import { GraphActions } from './GraphActions';
 
 export const GraphThunkActions = {
-  graphReady: (cyRef: any) => {
-    return (dispatch: KialiDispatch) => {
-      dispatch(
-        GraphActions.updateSummary({
-          summaryType: 'graph',
-          summaryTarget: cyRef
-        })
-      );
-    };
-  },
   graphPFReady: (controller: Controller) => {
     return (dispatch: KialiDispatch) => {
       dispatch(
         GraphActions.updateSummary({
-          isPF: true,
           summaryType: 'graph',
           summaryTarget: controller
         })
diff --git a/frontend/src/actions/MeshActions.ts b/frontend/src/actions/MeshActions.ts
index fabb167f2..981073f49 100644
--- a/frontend/src/actions/MeshActions.ts
+++ b/frontend/src/actions/MeshActions.ts
@@ -1,12 +1,12 @@
 import { ActionType, createStandardAction } from 'typesafe-actions';
-import { Layout } from '../types/Graph';
 import { ActionKeys } from './ActionKeys';
 import { TimeInMilliseconds } from 'types/Common';
 import { MeshDefinition, MeshTarget } from 'types/Mesh';
+import { MeshLayout } from 'pages/Mesh/layouts/layoutFactory';
 
 export const MeshActions = {
   setDefinition: createStandardAction(ActionKeys.MESH_SET_DEFINITION)<MeshDefinition>(),
-  setLayout: createStandardAction(ActionKeys.MESH_SET_LAYOUT)<Layout>(),
+  setLayout: createStandardAction(ActionKeys.MESH_SET_LAYOUT)<MeshLayout>(),
   setTarget: createStandardAction(ActionKeys.MESH_SET_TARGET)<MeshTarget>(),
   setUpdateTime: createStandardAction(ActionKeys.MESH_SET_UPDATE_TIME)<TimeInMilliseconds>()
 };
diff --git a/frontend/src/actions/__tests__/GraphActions.test.ts b/frontend/src/actions/__tests__/GraphActions.test.ts
index 3e77a88c6..9214d3750 100644
--- a/frontend/src/actions/__tests__/GraphActions.test.ts
+++ b/frontend/src/actions/__tests__/GraphActions.test.ts
@@ -1,25 +1,26 @@
+import { Controller, GraphElement } from '@patternfly/react-topology';
 import { GraphActions } from '../GraphActions';
 import { GraphThunkActions } from '../GraphThunkActions';
 import { getType } from 'typesafe-actions';
 
 describe('GraphActions', () => {
   it('should build "update summary" action', () => {
-    const showAction = GraphActions.updateSummary({ summaryType: 'node', summaryTarget: 'target' });
+    const showAction = GraphActions.updateSummary({ summaryType: 'node', summaryTarget: {} as GraphElement });
     expect(showAction.type).toEqual(getType(GraphActions.updateSummary));
     expect(showAction.payload).toEqual({
       summaryType: 'node',
-      summaryTarget: 'target'
+      summaryTarget: {} as GraphElement
     });
   });
 
   it('should dispatch "update summary" action on render', () => {
     const dispatch = jest.fn();
-    GraphThunkActions.graphReady('cyRef')(dispatch);
+    GraphThunkActions.graphPFReady({} as Controller)(dispatch);
 
     expect(dispatch.mock.calls.length).toBe(1);
     expect(dispatch.mock.calls[0][0]).toEqual(
       GraphActions.updateSummary({
-        summaryTarget: 'cyRef',
+        summaryTarget: {} as Controller,
         summaryType: 'graph'
       })
     );
diff --git a/frontend/src/app/__tests__/App.test.tsx b/frontend/src/app/__tests__/App.test.tsx
index 9a6304613..baee630d4 100644
--- a/frontend/src/app/__tests__/App.test.tsx
+++ b/frontend/src/app/__tests__/App.test.tsx
@@ -3,20 +3,6 @@ import * as ReactDOM from 'react-dom';
 import { App } from '../App';
 
 it('renders full App without crashing', () => {
-  // Mock getComputedStyle: Cytoscape relies on the result of this to have a valid paddingXXX
-  // Current implementation returns '' which is parsed to float as NAN, breaking cytoscape.
-  // Uses the default implementation and ensures we are returning a valid paddingXXX
-  const defaultGetComputedStyle = window.getComputedStyle;
-  window.getComputedStyle = jest.fn().mockImplementation(element => {
-    const computedStyle = defaultGetComputedStyle(element);
-    for (const prop of ['paddingTop', 'paddingRight', 'paddingLeft', 'paddingBottom']) {
-      if (computedStyle[prop] === '') {
-        computedStyle[prop] = '0px';
-      }
-    }
-    return computedStyle;
-  });
-
   // jest.mock('../../services/Api');
 
   process.env.REACT_APP_NAME = 'kiali-ui-test';
diff --git a/frontend/src/components/CytoscapeGraph/ContextMenu/EdgeContextMenu.tsx b/frontend/src/components/CytoscapeGraph/ContextMenu/EdgeContextMenu.tsx
deleted file mode 100644
index d12a4a148..000000000
--- a/frontend/src/components/CytoscapeGraph/ContextMenu/EdgeContextMenu.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from 'react';
-import { kialiStyle } from 'styles/StyleUtils';
-import { prettyProtocol } from 'types/Graph';
-import { EdgeContextMenuProps } from '../CytoscapeContextMenu';
-import { getTitle } from 'pages/Graph/SummaryPanelCommon';
-import { renderBadgedName } from 'pages/Graph/SummaryLink';
-import { decoratedNodeData } from '../CytoscapeGraphUtils';
-import { EdgeSingular } from 'cytoscape';
-
-const contextMenu = kialiStyle({
-  fontSize: 'var(--graph-side-panel--font-size)',
-  textAlign: 'left'
-});
-
-export const EdgeContextMenu: React.FC<EdgeContextMenuProps> = (props: EdgeContextMenuProps) => {
-  return (
-    <div className={contextMenu}>
-      {getTitle(`Edge (${prettyProtocol(props.protocol)})`)}
-      {renderBadgedName(decoratedNodeData((props.element as EdgeSingular).source()), 'From:  ')}
-      {renderBadgedName(decoratedNodeData((props.element as EdgeSingular).target()), 'To:        ')}
-    </div>
-  );
-};
diff --git a/frontend/src/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx b/frontend/src/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx
deleted file mode 100644
index 4fb7f07f5..000000000
--- a/frontend/src/components/CytoscapeGraph/ContextMenu/NodeContextMenu.tsx
+++ /dev/null
@@ -1,443 +0,0 @@
-import * as React from 'react';
-import { connect } from 'react-redux';
-import { kialiStyle } from 'styles/StyleUtils';
-import { Spinner, Tooltip, TooltipPosition } from '@patternfly/react-core';
-import { ExternalLinkAltIcon } from '@patternfly/react-icons';
-import { router } from 'app/History';
-import { BoxByType, DecoratedGraphNodeData, NodeType } from 'types/Graph';
-import { TracingInfo } from 'types/TracingInfo';
-import { durationSelector } from 'store/Selectors';
-import { KialiAppState } from 'store/Store';
-import { isMultiCluster, Paths, serverConfig } from 'config';
-import { NodeContextMenuProps } from '../CytoscapeContextMenu';
-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 { useKialiTranslation } from 'utils/I18nUtils';
-
-type ReduxProps = {
-  duration: DurationInSeconds;
-  kiosk: string;
-  tracingInfo?: TracingInfo;
-  updateTime: TimeInMilliseconds;
-};
-
-// Note, in the below styles we assign colors to be consistent with PF Dropdown
-const contextMenu = kialiStyle({
-  fontSize: 'var(--graph-side-panel--font-size)',
-  textAlign: 'left'
-});
-
-const contextMenuHeader = kialiStyle({
-  fontSize: 'var(--graph-side-panel--font-size)',
-  marginBottom: '0.25rem',
-  textAlign: 'left'
-});
-
-const contextMenuSubTitle = kialiStyle({
-  color: PFColors.Color200,
-  fontWeight: 700,
-  paddingTop: '0.125rem',
-  paddingBottom: '0.25rem'
-});
-
-const contextMenuItem = kialiStyle({
-  textDecoration: 'none',
-  $nest: {
-    '&:hover': {
-      backgroundColor: PFColors.BackgroundColor200,
-      color: PFColors.Link
-    }
-  }
-});
-
-const contextMenuItemLink = kialiStyle({
-  color: PFColors.Color100
-});
-
-const hrStyle = kialiStyle({
-  border: 0,
-  borderTop: `1px solid ${PFColors.BorderColor100}`,
-  margin: '0.5rem 0 0.25rem 0'
-});
-
-type Props = NodeContextMenuProps & ReduxProps;
-type LinkParams = { cluster?: string; name: string; namespace: string; type: string };
-
-const getLinkParamsForNode = (node: DecoratedGraphNodeData): LinkParams | undefined => {
-  const namespace: string = node.isServiceEntry ? node.isServiceEntry.namespace : node.namespace;
-
-  let cluster = node.cluster;
-  let name: string | undefined = undefined;
-  let type: string | undefined = undefined;
-
-  switch (node.nodeType) {
-    case NodeType.APP:
-    case NodeType.BOX:
-      // only app boxes have full context menus
-      const isBox = node.isBox;
-
-      if (!isBox || isBox === BoxByType.APP) {
-        // Prefer workload links
-        if (node.workload && node.parent) {
-          name = node.workload;
-          type = Paths.WORKLOADS;
-        } else {
-          type = Paths.APPLICATIONS;
-          name = node.app;
-        }
-      }
-      break;
-    case NodeType.SERVICE:
-      type = node.isServiceEntry ? Paths.SERVICEENTRIES : Paths.SERVICES;
-      name = node.service;
-      break;
-    case NodeType.WORKLOAD:
-      name = node.workload;
-      type = Paths.WORKLOADS;
-      break;
-  }
-
-  return type && name ? { namespace, type, name, cluster } : undefined;
-};
-
-export const NodeContextMenuComponent: React.FC<Props> = (props: Props) => {
-  const { t } = useKialiTranslation();
-
-  const [serviceDetails, gateways, peerAuthentications, isServiceDetailsLoading] = useServiceDetailForGraphNode(
-    props,
-    !props.isInaccessible,
-    props.duration,
-    props.updateTime
-  );
-
-  const updateLabel = getServiceDetailsUpdateLabel(serviceDetails);
-
-  // TODO: Deduplicate
-  const getDropdownItemTooltipMessage = (): string => {
-    if (serverConfig.deployment.viewOnlyMode) {
-      return t('User does not have permission');
-    } else if (hasServiceDetailsTrafficRouting(serviceDetails)) {
-      return t('Traffic routing already exists for this service');
-    } else {
-      return t("Traffic routing doesn't exist for this service");
-    }
-  };
-
-  const createMenuItem = (href: string, title: string, target = '_self', external = false): React.ReactNode => {
-    const commonLinkProps = {
-      className: contextMenuItemLink,
-      children: title,
-      target
-    };
-
-    let item: any;
-
-    if (external) {
-      item = (
-        <a href={href} rel="noreferrer noopener" {...commonLinkProps}>
-          {commonLinkProps.children} <ExternalLinkAltIcon />
-        </a>
-      );
-    } else {
-      // 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 = (
-          <a
-            onClick={() => {
-              kioskContextMenuAction(href);
-            }}
-            className={commonLinkProps.className}
-            children={commonLinkProps.children}
-          />
-        );
-      } else {
-        item = <a {...commonLinkProps} onClick={() => onClick(href)} />;
-      }
-    }
-
-    return (
-      <div key={title} className={contextMenuItem}>
-        {item}
-      </div>
-    );
-  };
-
-  const onClick = (href: string): void => {
-    props.contextMenu.hide(0);
-    router.navigate(href);
-  };
-
-  const handleClickWizard = (e: React.MouseEvent<HTMLAnchorElement>, eventKey: WizardAction): void => {
-    e.preventDefault();
-    props.contextMenu.hide(0);
-
-    if (props.onLaunchWizard && serviceDetails && gateways && peerAuthentications) {
-      props.onLaunchWizard(
-        eventKey,
-        updateLabel.length === 0 ? 'create' : 'update',
-        props.namespace,
-        serviceDetails,
-        gateways,
-        peerAuthentications
-      );
-    }
-  };
-
-  const handleDeleteTrafficRouting = (e: React.MouseEvent<HTMLAnchorElement>): void => {
-    e.preventDefault();
-    props.contextMenu.hide(0);
-
-    if (props.onDeleteTrafficRouting && serviceDetails) {
-      props.onDeleteTrafficRouting(DELETE_TRAFFIC_ROUTING, serviceDetails);
-    }
-  };
-
-  const renderHeader = (): React.ReactNode => {
-    return (
-      <>
-        {props.isBox ? getTitle(props.isBox) : getTitle(props.nodeType)}
-
-        {(!props.isBox || props.isBox === BoxByType.APP) && (
-          <div className={contextMenuHeader}>
-            <PFBadge badge={PFBadges.Namespace} size="sm" />
-            {props.namespace}
-          </div>
-        )}
-
-        {renderBadgedName(props)}
-      </>
-    );
-  };
-
-  const renderWizardActionItem = (eventKey: string): React.ReactNode => {
-    const enabledItem =
-      !hasServiceDetailsTrafficRouting(serviceDetails) ||
-      (hasServiceDetailsTrafficRouting(serviceDetails) && updateLabel === eventKey);
-
-    // An Item is enabled under two conditions:
-    // a) No traffic -> Wizard can create new one
-    // b) Existing traffic generated by the traffic -> Wizard can update that scenario
-    // Otherwise, the item should be disabled
-    if (!enabledItem) {
-      return (
-        <div key={eventKey} className={contextMenuItem} style={{ color: PFColors.Color200 }}>
-          <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)}
-          >
-            {WIZARD_TITLES[eventKey]}
-          </a>
-        </div>
-      );
-    }
-  };
-
-  const renderDeleteTrafficRoutingItem = (): React.ReactNode => {
-    if (
-      !canDelete(serviceDetails?.istioPermissions) ||
-      !hasServiceDetailsTrafficRouting(serviceDetails) /*|| props.isDisabled*/
-    ) {
-      return (
-        <div className={contextMenuItem} style={{ color: PFColors.Color200 }}>
-          <Tooltip position={TooltipPosition.left} content={<>{getDropdownItemTooltipMessage()}</>}>
-            <div style={{ display: 'inline-block', cursor: 'not-allowed' }}>Delete Traffic Routing</div>
-          </Tooltip>
-        </div>
-      );
-    } else {
-      return (
-        <div className={contextMenuItem}>
-          <a
-            href="#"
-            rel="noreferrer noopener"
-            className={contextMenuItemLink}
-            onClick={handleDeleteTrafficRouting}
-            data-test="delete-traffic-routing"
-          >
-            Delete Traffic Routing
-          </a>
-        </div>
-      );
-    }
-  };
-
-  const renderWizardsItems = (): React.ReactNode | null => {
-    if (isServiceDetailsLoading) {
-      return (
-        <>
-          <hr className={hrStyle} />
-          <div className={contextMenuSubTitle}>Actions</div>
-          <div className={contextMenuItem}>
-            <Spinner size="md" aria-label={t('Loading actions...')} />
-          </div>
-        </>
-      );
-    }
-
-    if (serviceDetails) {
-      return (
-        <>
-          <hr className={hrStyle} />
-          <div className={contextMenuSubTitle}>{updateLabel === '' ? t('Create') : t('Update')}</div>
-          {SERVICE_WIZARD_ACTIONS.map(eventKey => renderWizardActionItem(eventKey))}
-          <hr className={hrStyle} />
-          {renderDeleteTrafficRoutingItem()}
-        </>
-      );
-    }
-
-    return null;
-  };
-
-  if (props.isInaccessible) {
-    props.contextMenu.disable();
-    return null;
-  }
-
-  // render()
-  if (props.isHover) {
-    return <div className={contextMenu}>{renderHeader()}</div>;
-  }
-
-  const linkParams = getLinkParamsForNode(props);
-  // Disable context menu if we are dealing with an aggregate (currently has no detail)
-  if (!linkParams) {
-    props.contextMenu.disable();
-    return null;
-  }
-
-  // The getOptionsFromLinkParams function can potentially return a blank list if the
-  // node associated to the context menu is for a remote cluster with no accessible Kialis.
-  // That would lead to an empty menu. Here, we assume that whoever is the host/parent component,
-  // that component won't render this context menu in case this menu would be blank. So, here
-  // it's simply assumed that the context menu will look good.
-  const options: ContextMenuOption[] = getOptionsFromLinkParams(linkParams, props.tracingInfo);
-  const menuOptions = (
-    <>
-      <div className={contextMenuSubTitle}>{t('Show')}</div>
-      {options.map(o => createMenuItem(o.url, o.text, o.target, o.external))}
-    </>
-  );
-
-  return (
-    <div className={contextMenu} data-test="graph-node-context-menu">
-      {renderHeader()}
-      <hr className={hrStyle} />
-      {menuOptions}
-      {renderWizardsItems()}
-    </div>
-  );
-};
-
-const getTracingURL = (namespace: string, namespaceSelector: boolean, tracingURL: string, name?: string): string => {
-  return `${tracingURL}/search?service=${name}${namespaceSelector ? `.${namespace}` : ''}`;
-};
-
-export type ContextMenuOption = {
-  external?: boolean;
-  target?: string;
-  text: string;
-  url: string;
-};
-
-export const clickHandler = (o: ContextMenuOption, kiosk: string): void => {
-  if (o.external) {
-    window.open(o.url, o.target);
-  } else {
-    if (isParentKiosk(kiosk)) {
-      kioskContextMenuAction(o.url);
-    } else {
-      router.navigate(o.url);
-    }
-  }
-};
-
-export const getOptions = (node: DecoratedGraphNodeData, tracingInfo?: TracingInfo): ContextMenuOption[] => {
-  if (node.isInaccessible) {
-    return [];
-  }
-
-  const linkParams = getLinkParamsForNode(node);
-
-  if (!linkParams) {
-    return [];
-  }
-
-  return getOptionsFromLinkParams(linkParams, tracingInfo);
-};
-
-const getOptionsFromLinkParams = (linkParams: LinkParams, tracingInfo?: TracingInfo): ContextMenuOption[] => {
-  const { namespace, type, name, cluster } = linkParams;
-
-  let options: ContextMenuOption[] = [];
-  let detailsPageUrl = `/namespaces/${namespace}/${type}/${name}`;
-  let concat = '?';
-
-  if (cluster && isMultiCluster) {
-    detailsPageUrl += `?clusterName=${cluster}`;
-    concat = '&';
-  }
-
-  options.push({ text: 'Details', url: detailsPageUrl });
-
-  if (type !== Paths.SERVICEENTRIES) {
-    options.push({ text: 'Traffic', url: `${detailsPageUrl}${concat}tab=traffic` });
-
-    if (type === Paths.WORKLOADS) {
-      options.push({ text: 'Logs', url: `${detailsPageUrl}${concat}tab=logs` });
-    }
-
-    options.push({
-      text: 'Inbound Metrics',
-      url: `${detailsPageUrl}${concat}tab=${type === Paths.SERVICES ? 'metrics' : 'in_metrics'}`
-    });
-
-    if (type !== Paths.SERVICES) {
-      options.push({ text: 'Outbound Metrics', url: `${detailsPageUrl}${concat}tab=out_metrics` });
-    }
-
-    if (type === Paths.APPLICATIONS && tracingInfo && tracingInfo.enabled) {
-      if (tracingInfo.integration) {
-        options.push({ text: 'Traces', url: `${detailsPageUrl}${concat}tab=traces` });
-      } else if (tracingInfo.url) {
-        options.push({
-          text: 'Show Traces',
-          url: getTracingURL(namespace, tracingInfo.namespaceSelector, tracingInfo.url, name),
-          external: true,
-          target: '_blank'
-        });
-      }
-    }
-  }
-
-  return options;
-};
-
-const mapStateToProps = (state: KialiAppState): ReduxProps => ({
-  duration: durationSelector(state),
-  updateTime: state.graph.updateTime,
-  tracingInfo: state.tracingState.info,
-  kiosk: state.globalState.kiosk
-});
-
-export const NodeContextMenu = connect(mapStateToProps)(NodeContextMenuComponent);
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeContextMenu.tsx b/frontend/src/components/CytoscapeGraph/CytoscapeContextMenu.tsx
deleted file mode 100644
index f561d8a39..000000000
--- a/frontend/src/components/CytoscapeGraph/CytoscapeContextMenu.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-import * as React from 'react';
-import * as ReactDOM from 'react-dom';
-import * as Cy from 'cytoscape';
-import { RouterProvider } from 'react-router-dom-v5-compat';
-import tippy, { Instance } from 'tippy.js';
-import { DecoratedGraphEdgeData, DecoratedGraphNodeData } from '../../types/Graph';
-import { PeerAuthentication } from '../../types/IstioObjects';
-import { ServiceDetailsInfo } from '../../types/ServiceInfo';
-import { Provider } from 'react-redux';
-import { store } from '../../store/ConfigStore';
-import { createRouter } from '../../app/History';
-import { getOptions } from './ContextMenu/NodeContextMenu';
-import { WizardAction, WizardMode } from '../IstioWizards/WizardActions';
-import { Theme } from 'types/Common';
-import { pathRoutes } from 'routes';
-
-export type EdgeContextMenuProps = DecoratedGraphEdgeData & ContextMenuProps;
-export type EdgeContextMenuComponentType = React.ComponentType<EdgeContextMenuProps>;
-export type NodeContextMenuProps = DecoratedGraphNodeData &
-  ContextMenuProps & {
-    onDeleteTrafficRouting?: (key: string, serviceDetails: ServiceDetailsInfo) => void;
-    onLaunchWizard?: (
-      key: WizardAction,
-      mode: WizardMode,
-      namespace: string,
-      serviceDetails: ServiceDetailsInfo,
-      gateways: string[],
-      peerAuths: PeerAuthentication[]
-    ) => void;
-  };
-export type NodeContextMenuComponentType = React.ComponentType<NodeContextMenuProps>;
-export type ContextMenuComponentType = EdgeContextMenuComponentType | NodeContextMenuComponentType;
-
-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;
-  theme: string;
-};
-
-type TippyInstance = Instance;
-
-type ContextMenuContainer = HTMLDivElement & {
-  _contextMenu: TippyInstance;
-};
-
-type ContextMenuProps = {
-  contextMenu: TippyInstance;
-  element: Cy.NodeSingular | Cy.EdgeSingular;
-  isHover: boolean;
-};
-
-export class CytoscapeContextMenuWrapper extends React.PureComponent<Props> {
-  private readonly contextMenuRef: React.RefObject<ContextMenuContainer>;
-  private isHover: boolean | undefined;
-
-  constructor(props: Props) {
-    super(props);
-    this.contextMenuRef = React.createRef<ContextMenuContainer>();
-    this.isHover = undefined;
-  }
-
-  componentDidMount(): void {
-    document.addEventListener('mouseup', this.handleDocumentMouseUp);
-  }
-
-  componentWillUnmount(): void {
-    document.removeEventListener('mouseup', this.handleDocumentMouseUp);
-  }
-
-  render(): React.ReactNode {
-    return (
-      <div className="hidden">
-        <div ref={this.contextMenuRef} />
-      </div>
-    );
-  }
-
-  // Add cy listener for context menu events on nodes and edges
-  connectCy = (cy: Cy.Core): void => {
-    cy.on('cxttapstart', 'node,edge', (event: Cy.EventObject) => {
-      event.preventDefault();
-
-      if (event.target) {
-        this.handleContextMenu(event.target, false);
-      }
-
-      return false;
-    });
-  };
-
-  // Connects cy to this component
-  handleContextMenu = (elem: Cy.NodeSingular | Cy.EdgeSingular, isHover: boolean): void => {
-    const contextMenuType = elem.isNode() ? this.props.contextMenuNodeComponent : this.props.contextMenuEdgeComponent;
-
-    if (contextMenuType) {
-      this.makeContextMenu(contextMenuType, elem, isHover, elem.isNode());
-    }
-  };
-
-  hideContextMenu = (isHover: boolean | undefined): void => {
-    const currentContextMenu = this.getCurrentContextMenu();
-
-    if (currentContextMenu) {
-      if (!isHover || this.isHover) {
-        currentContextMenu.hide(0); // hide it in 0ms
-        this.isHover = undefined;
-        ReactDOM.unmountComponentAtNode(this.contextMenuRef.current as HTMLDivElement);
-      }
-    }
-  };
-
-  private handleDocumentMouseUp = (event: MouseEvent): void => {
-    if (event.button === 2) {
-      // Ignore mouseup of right button
-      return;
-    }
-
-    const currentContextMenu: Instance | undefined = this.getCurrentContextMenu();
-
-    if (currentContextMenu) {
-      // Allow interaction in our popper component (Selecting and copying) without it disappearing
-      if (event.target && currentContextMenu.popper.contains(event.target as Node)) {
-        return;
-      }
-
-      this.hideContextMenu(this.isHover);
-    }
-  };
-
-  private makeContextMenu = (
-    ContextMenuComponentType: ContextMenuComponentType,
-    target: Cy.NodeSingular | Cy.EdgeSingular,
-    isHover: boolean,
-    isNode: boolean
-  ): void => {
-    // Don't let a hover trump a non-hover context menu
-    if (isHover && this.isHover === false) {
-      return;
-    }
-    // If there is no valid context menu just return
-    if (!isHover && (target.isEdge() || getOptions({ ...target.data() }).length === 0)) {
-      return;
-    }
-
-    // hide any existing context menu
-    this.hideContextMenu(isHover);
-
-    // Prevent the tippy content from picking up the right-click when we are moving it over to the edge/node
-    this.addContextMenuEventListener();
-    const content = this.contextMenuRef.current;
-
-    const tippyInstance = tippy(
-      (target as any).popperRef(), // Using an extension, popperRef is not in base definition
-      {
-        content: content as HTMLDivElement,
-        trigger: 'manual',
-        arrow: true,
-        placement: 'bottom',
-        hideOnClick: false,
-        multiple: false,
-        sticky: true,
-        interactive: true,
-        theme: this.props.theme === Theme.DARK ? '' : 'light-border',
-        size: 'large',
-        distance: this.tippyDistance(target)
-      }
-    ).instances[0];
-
-    let menuComponent = (
-      <ContextMenuComponentType element={target} contextMenu={tippyInstance} isHover={isHover} {...target.data()} />
-    );
-
-    if (isNode) {
-      menuComponent = (
-        <ContextMenuComponentType
-          element={target}
-          contextMenu={tippyInstance}
-          isHover={isHover}
-          onDeleteTrafficRouting={this.props.onDeleteTrafficRouting}
-          onLaunchWizard={this.props.onLaunchWizard}
-          {...target.data()}
-        />
-      );
-    }
-
-    const contextMenuRouter = createRouter([
-      {
-        element: menuComponent,
-        children: pathRoutes
-      }
-    ]);
-
-    const result = (
-      <Provider store={store}>
-        <RouterProvider router={contextMenuRouter} />
-      </Provider>
-    );
-
-    // save the context menu type to make sure we don't hide full context menus
-    this.isHover = isHover;
-
-    ReactDOM.render(result, content, () => {
-      this.setCurrentContextMenu(tippyInstance);
-      tippyInstance.show();
-
-      // Schedule the removal of the contextmenu listener after finishing with the show procedure, so we can
-      // interact with the popper content e.g. select and copy (with right click) values from it.
-      setTimeout(() => {
-        this.removeContextMenuEventListener();
-      }, 0);
-    });
-  };
-
-  private getCurrentContextMenu = (): Instance | undefined => {
-    return this.contextMenuRef?.current?._contextMenu;
-  };
-
-  private setCurrentContextMenu = (current: TippyInstance): void => {
-    this.contextMenuRef!.current!._contextMenu = current;
-  };
-
-  private addContextMenuEventListener = (): void => {
-    document.addEventListener('contextmenu', this.handleContextMenuEvent);
-  };
-
-  private removeContextMenuEventListener = (): void => {
-    document.removeEventListener('contextmenu', this.handleContextMenuEvent);
-  };
-
-  private handleContextMenuEvent = (event: MouseEvent): boolean => {
-    // Disable the context menu in popper
-    const currentContextMenu = this.getCurrentContextMenu();
-
-    if (currentContextMenu) {
-      if (event.target && currentContextMenu.popper.contains(event.target as Node)) {
-        event.preventDefault();
-      }
-    }
-
-    return true;
-  };
-
-  private tippyDistance = (_target: Cy.NodeSingular | Cy.EdgeSingular): number => {
-    return 10;
-  };
-}
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeGraph.tsx b/frontend/src/components/CytoscapeGraph/CytoscapeGraph.tsx
deleted file mode 100644
index 5d0ca371a..000000000
--- a/frontend/src/components/CytoscapeGraph/CytoscapeGraph.tsx
+++ /dev/null
@@ -1,1075 +0,0 @@
-import * as Cy from 'cytoscape';
-import { Core, EdgeSingular, NodeSingular } from 'cytoscape';
-import * as React from 'react';
-import ReactResizeDetector from 'react-resize-detector';
-import { GraphData } from 'pages/Graph/GraphPage';
-import { IntervalInMilliseconds, TimeInMilliseconds } from '../../types/Common';
-import {
-  BoxByType,
-  CLUSTER_DEFAULT,
-  CytoscapeBaseEvent,
-  GraphEvent,
-  CytoscapeGlobalScratchData,
-  CytoscapeGlobalScratchNamespace,
-  EdgeLabelMode,
-  EdgeMode,
-  Layout,
-  NodeParamsType,
-  NodeType,
-  RankMode,
-  RankResult,
-  SummaryData,
-  UNKNOWN,
-  NodeAttr
-} from '../../types/Graph';
-import { JaegerTrace } from 'types/TracingInfo';
-import { Namespace } from '../../types/Namespace';
-import { addInfo } from 'utils/AlertUtils';
-import { angleBetweenVectors, squaredDistance, normalize } from '../../utils/MathUtils';
-import { WizardAction, WizardMode } from '../IstioWizards/WizardActions';
-import {
-  CytoscapeContextMenuWrapper,
-  NodeContextMenuComponentType,
-  EdgeContextMenuComponentType
-} from './CytoscapeContextMenu';
-import * as CytoscapeGraphUtils from './CytoscapeGraphUtils';
-import { isCore, isEdge, isNode } from './CytoscapeGraphUtils';
-import { CytoscapeReactWrapper } from './CytoscapeReactWrapper';
-import { showTrace, hideTrace } from './CytoscapeTrace';
-import { EmptyGraphLayout } from './EmptyGraphLayout';
-import { FocusAnimation } from './FocusAnimation';
-import { GraphHighlighter } from './graphs/GraphHighlighter';
-import { TrafficRenderer } from './TrafficAnimation/TrafficRenderer';
-import { homeCluster, 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';
-
-type CytoscapeGraphProps = {
-  containerClassName?: string;
-  contextMenuEdgeComponent?: EdgeContextMenuComponentType;
-  contextMenuNodeComponent?: NodeContextMenuComponentType;
-  edgeLabels: EdgeLabelMode[];
-  edgeMode: EdgeMode;
-  focusSelector?: string;
-  graphData: GraphData;
-  isMiniGraph: boolean;
-  layout: Layout;
-  namespaceLayout: Layout;
-  onEmptyGraphAction?: () => void;
-  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;
-  onNodeTap?: (e: GraphNodeTapEvent) => void;
-  onReady?: (cytoscapeRef: any) => void;
-  rankBy: RankMode[];
-  refreshInterval: IntervalInMilliseconds;
-  setActiveNamespaces?: (namespace: Namespace[]) => void;
-  setNode?: (node?: NodeParamsType) => void;
-  setRankResult?: (result: RankResult) => void;
-  setTraceId?: (traceId?: string) => void;
-  setUpdateTime?: (val: TimeInMilliseconds) => void;
-  showIdleEdges: boolean;
-  showIdleNodes: boolean;
-  showOutOfMesh: boolean;
-  showOperationNodes: boolean;
-  showRank: boolean;
-  showSecurity: boolean;
-  showServiceNodes: boolean;
-  showTrafficAnimation: boolean;
-  showVirtualServices: boolean;
-  summaryData: SummaryData | null;
-  toggleIdleNodes: () => void;
-  trace?: JaegerTrace;
-  updateSummary?: (event: GraphEvent) => void;
-  theme: string;
-};
-
-// This is a Cypress test hook. Cypress-react-selector can access the react node state, and so
-// by offering `cy`, we can validate graph impact of test automation actions.  Note that state updates
-// do not cause component updates (see shouldComponentUpdate)
-type CytoscapeGraphState = {
-  cy: Cy.Core | null;
-};
-
-export interface GraphEdgeTapEvent {
-  namespace: string;
-  type: string;
-  source: string;
-  target: string;
-}
-
-export interface GraphNodeTapEvent {
-  aggregate?: string;
-  aggregateValue?: string;
-  app: string;
-  cluster?: string;
-  isBox?: string;
-  isInaccessible: boolean;
-  isOutOfMesh: boolean;
-  isOutside: boolean;
-  isServiceEntry: boolean;
-  isIdle: boolean;
-  namespace: string;
-  nodeType: NodeType;
-  service: string;
-  version?: string;
-  workload: string;
-}
-
-export interface GraphNodeDoubleTapEvent extends GraphNodeTapEvent {}
-
-// exporting this class for testing
-export class CytoscapeGraph extends React.Component<CytoscapeGraphProps, CytoscapeGraphState> {
-  static contextTypes = {
-    router: () => null
-  };
-  static defaultProps = {
-    isMiniGraph: false
-  };
-
-  // for hover support
-  static hoverInMs = 260;
-  static hoverOutMs = 100;
-  static mouseInTarget: any;
-  static mouseInTimeout: any;
-  static mouseOutTimeout: any;
-
-  // for dbl-click support
-  static doubleTapMs = 350;
-  static tapTarget: any;
-  static tapTimeout: any;
-  static readonly DataNodeId = 'data-node-id';
-
-  private readonly contextMenuRef: React.RefObject<CytoscapeContextMenuWrapper>;
-  private cy?: Cy.Core;
-  private customViewport: boolean;
-  private cytoscapeGraphRef: any;
-  private cytoscapeReactWrapperRef: any;
-  private focusSelector?: string;
-  private graphHighlighter?: GraphHighlighter;
-  private needsInitialLayout: boolean;
-  private nodeChanged: boolean;
-  private trafficRenderer?: TrafficRenderer;
-  private userBoxSelected?: Cy.Collection;
-  private zoom: number; // the current zoom value, used for checking threshold crossing
-  private zoomIgnore: boolean; // used to ignore zoom events when cy sometimes generates 'intermediate' values
-  private zoomThresholds: number[];
-
-  constructor(props: CytoscapeGraphProps) {
-    super(props);
-    this.contextMenuRef = React.createRef<CytoscapeContextMenuWrapper>();
-    this.customViewport = false;
-    this.cytoscapeGraphRef = React.createRef<CytoscapeGraph>();
-    this.cytoscapeReactWrapperRef = React.createRef();
-    this.focusSelector = props.focusSelector;
-    this.needsInitialLayout = false;
-    this.nodeChanged = false;
-    this.state = {
-      cy: null
-    };
-    this.zoom = 1; // 1 is the default cy zoom
-    this.zoomIgnore = true; // ignore zoom events prior to the first rendering
-    const settings = serverConfig.kialiFeatureFlags.uiDefaults.graph.settings;
-    this.zoomThresholds = Array.from(
-      new Set([settings.minFontLabel / settings.fontLabel, settings.minFontBadge / settings.fontLabel])
-    );
-  }
-
-  componentDidMount() {
-    this.cyInitialization(this.getCy()!);
-  }
-
-  shouldComponentUpdate(nextProps: CytoscapeGraphProps) {
-    this.nodeChanged =
-      this.nodeChanged || this.props.graphData.fetchParams.node !== nextProps.graphData.fetchParams.node;
-
-    // only update on display changes for the existing graph. Duration or refreshInterval changes don't
-    // affect display. Options that trigger a graph refresh will have an update when the refresh
-    // completes (showIdleNodes, showSecurity, showServiceNodes, etc).
-    let result =
-      this.props.edgeLabels !== nextProps.edgeLabels ||
-      this.props.graphData.isLoading !== nextProps.graphData.isLoading ||
-      this.props.graphData.elements !== nextProps.graphData.elements ||
-      this.props.layout !== nextProps.layout ||
-      this.props.namespaceLayout !== nextProps.namespaceLayout ||
-      this.props.rankBy !== nextProps.rankBy ||
-      this.props.showOutOfMesh !== nextProps.showOutOfMesh ||
-      this.props.showRank !== nextProps.showRank ||
-      this.props.showTrafficAnimation !== nextProps.showTrafficAnimation ||
-      this.props.showVirtualServices !== nextProps.showVirtualServices ||
-      this.props.trace !== nextProps.trace;
-
-    return result;
-  }
-
-  componentDidUpdate(prevProps: CytoscapeGraphProps) {
-    const cy = this.getCy();
-    if (!cy) {
-      return;
-    }
-    if (this.props.graphData.isLoading) {
-      return;
-    }
-
-    // Check to see if we should run a layout when we process the graphUpdate
-    let runLayout = false;
-    const newLayout =
-      this.props.layout.name !== prevProps.layout.name ||
-      this.props.namespaceLayout.name !== prevProps.namespaceLayout.name;
-
-    if (this.needsInitialLayout || newLayout || this.props.graphData.elementsChanged || this.nodeNeedsRelayout()) {
-      this.needsInitialLayout = false;
-      runLayout = true;
-    }
-
-    cy.emit('kiali-zoomignore', [true]);
-    this.processGraphUpdate(cy, runLayout, newLayout).then(_response => {
-      // pre-select node if provided
-      const node = this.props.graphData.fetchParams.node;
-      if (node && cy && cy.$(':selected').length === 0) {
-        let selector = `[namespace = "${node.namespace.name}"][nodeType = "${node.nodeType}"]`;
-        switch (node.nodeType) {
-          case NodeType.AGGREGATE:
-            selector =
-              selector + "[aggregate = '" + node.aggregate! + "'][aggregateValue = '" + node.aggregateValue! + "']";
-            break;
-          case NodeType.APP:
-          case NodeType.BOX: // we only support app box node graphs, treat like an app node
-            selector = selector + "[app = '" + node.app + "']";
-            if (node.version && node.version !== UNKNOWN) {
-              selector = selector + "[version = '" + node.version + "']";
-            }
-            break;
-          case NodeType.SERVICE:
-            selector = selector + "[service = '" + node.service + "']";
-            break;
-          default:
-            selector = selector + "[workload = '" + node.workload + "']";
-        }
-
-        const eles = cy.nodes(selector);
-        if (eles.length > 0) {
-          let target = eles[0];
-          // default app to the whole app box, when appropriate
-          if (
-            (node.nodeType === NodeType.APP || node.nodeType === NodeType.BOX) &&
-            !node.version &&
-            target.isChild() &&
-            target.parent()[0].data(NodeAttr.isBox) === BoxByType.APP
-          ) {
-            target = target.parent()[0];
-          }
-
-          this.selectTargetAndUpdateSummary(target);
-        }
-      }
-
-      if (this.props.trace) {
-        showTrace(cy, this.props.graphData.fetchParams.graphType, this.props.trace);
-      } else if (!this.props.trace && prevProps.trace) {
-        hideTrace(cy);
-      }
-    });
-  }
-
-  componentWillUnmount() {
-    if (CytoscapeGraph.mouseInTimeout) {
-      clearTimeout(CytoscapeGraph.mouseInTimeout);
-      CytoscapeGraph.mouseInTimeout = null;
-    }
-    if (CytoscapeGraph.mouseOutTimeout) {
-      clearTimeout(CytoscapeGraph.mouseOutTimeout);
-      CytoscapeGraph.mouseOutTimeout = null;
-    }
-  }
-
-  render() {
-    return (
-      <div id="cytoscape-graph" className={this.props.containerClassName} ref={this.cytoscapeGraphRef}>
-        <ReactResizeDetector handleWidth={true} handleHeight={true} skipOnMount={false} onResize={this.onResize} />
-        <EmptyGraphLayout
-          action={this.props.onEmptyGraphAction}
-          elements={this.props.graphData.elements}
-          error={this.props.graphData.errorMessage}
-          isLoading={this.props.graphData.isLoading}
-          isError={!!this.props.graphData.isError}
-          isMiniGraph={this.props.isMiniGraph}
-          namespaces={this.props.graphData.fetchParams.namespaces}
-          showIdleNodes={this.props.showIdleNodes}
-          toggleIdleNodes={this.props.toggleIdleNodes}
-        >
-          <CytoscapeContextMenuWrapper
-            ref={this.contextMenuRef}
-            contextMenuEdgeComponent={this.props.contextMenuEdgeComponent}
-            contextMenuNodeComponent={this.props.contextMenuNodeComponent}
-            onDeleteTrafficRouting={this.props.onDeleteTrafficRouting}
-            onLaunchWizard={this.props.onLaunchWizard}
-            theme={this.props.theme}
-          />
-          <CytoscapeReactWrapper ref={e => this.setCytoscapeReactWrapperRef(e)} />
-        </EmptyGraphLayout>
-      </div>
-    );
-  }
-
-  getCy(): Cy.Core | null {
-    return this.cytoscapeReactWrapperRef.current ? this.cytoscapeReactWrapperRef.current.getCy() : null;
-  }
-
-  static buildTapEventArgs(event: GraphEvent): GraphNodeTapEvent | GraphEdgeTapEvent {
-    const target = event.summaryTarget;
-    const targetType = event.summaryType;
-    const targetOrBoxChildren = targetType === 'box' ? target.descendants() : target;
-
-    if (targetType === 'edge') {
-      const nodeSource = decoratedNodeData(target.source());
-      const nodeTarget = decoratedNodeData(target.target());
-      return {
-        namespace: nodeSource.namespace,
-        type: nodeSource.nodeType,
-        source: nodeSource[nodeSource.nodeType],
-        target: nodeTarget[nodeTarget.nodeType]
-      };
-    }
-    // Invoke callback
-    return {
-      aggregate: target.data(NodeAttr.aggregate),
-      aggregateValue: target.data(NodeAttr.aggregateValue),
-      app: target.data(NodeAttr.app),
-      cluster: target.data(NodeAttr.cluster),
-      isBox: target.data(NodeAttr.isBox),
-      isIdle: targetOrBoxChildren.every(t => t.data(NodeAttr.isIdle)),
-      isInaccessible: target.data(NodeAttr.isInaccessible),
-      isOutOfMesh: target.data(NodeAttr.isOutOfMesh),
-      isOutside: target.data(NodeAttr.isOutside),
-      isServiceEntry: target.data(NodeAttr.isServiceEntry),
-      namespace: target.data(NodeAttr.namespace),
-      nodeType: target.data(NodeAttr.nodeType),
-      service: target.data(NodeAttr.service),
-      version: targetType === 'box' ? undefined : target.data(NodeAttr.version),
-      workload: target.data(NodeAttr.workload)
-    };
-  }
-
-  private setCytoscapeReactWrapperRef(cyRef: any) {
-    if (this.cytoscapeReactWrapperRef.current !== cyRef) {
-      this.cytoscapeReactWrapperRef.current = cyRef;
-      const cy = this.getCy();
-      this.cyInitialization(cy!);
-      this.setState({ cy: cy });
-    }
-  }
-
-  private onResize = () => {
-    if (this.cy) {
-      this.cy.resize();
-      // always fit to the newly sized space
-      this.safeFit(this.cy, true);
-    }
-  };
-
-  private cyInitialization(cy: Cy.Core) {
-    if (!cy) {
-      return;
-    }
-
-    // Caches the cy instance that is currently in use.
-    // If that cy instance is the same one we are being asked to initialize, do NOT initialize it again;
-    // this would add duplicate callbacks and would screw up the graph highlighter. If, however,
-    // we are being asked to initialize a different cy instance, we assume the current one is now obsolete
-    // so we do want to initialize the new cy instance.
-    if (this.cy === cy) {
-      return;
-    }
-    this.cy = cy;
-    this.cy.boxSelectionEnabled(true);
-
-    this.contextMenuRef!.current!.connectCy(this.cy);
-
-    this.graphHighlighter = new GraphHighlighter(cy);
-    this.trafficRenderer = new TrafficRenderer(cy);
-
-    const getCytoscapeBaseEvent = (event: Cy.EventObject): CytoscapeBaseEvent | null => {
-      const target = event.target;
-      if (target === cy) {
-        return { summaryType: 'graph', summaryTarget: cy };
-      } else if (isNode(target)) {
-        if (target.data(NodeAttr.isBox)) {
-          return { summaryType: 'box', summaryTarget: target };
-        } else {
-          return { summaryType: 'node', summaryTarget: target };
-        }
-      } else if (isEdge(target)) {
-        return { summaryType: 'edge', summaryTarget: target };
-      } else {
-        return null;
-      }
-    };
-
-    const findRelatedNode = element => {
-      // Skip top-level node, this one has margins that we don't want to consider.
-      if (element.getAttribute(CytoscapeGraph.DataNodeId)) {
-        return null;
-      }
-      while (element && element.getAttribute) {
-        const dataNodeId = element.getAttribute(CytoscapeGraph.DataNodeId);
-        if (dataNodeId) {
-          return dataNodeId;
-        }
-        element = element.parentNode;
-      }
-      return null;
-    };
-
-    cy.on('tap', (event: Cy.EventObject) => {
-      // Check if we clicked a label, if so stop processing the event right away.
-      if (event.originalEvent) {
-        const element = document.elementFromPoint(event.originalEvent.clientX, event.originalEvent.clientY);
-        const realTargetId = findRelatedNode(element);
-        if (realTargetId) {
-          const realTarget = cy.$id(realTargetId);
-          if (realTarget) {
-            event.preventDefault();
-            realTarget.trigger('tap');
-            return;
-          }
-        }
-      }
-      let tapped: NodeSingular | EdgeSingular | Core | null = event.target;
-      if (CytoscapeGraph.tapTimeout) {
-        // cancel any single-tap timer in progress
-        clearTimeout(CytoscapeGraph.tapTimeout);
-        CytoscapeGraph.tapTimeout = null;
-
-        // cancel any active hover timers
-        if (CytoscapeGraph.mouseInTimeout) {
-          clearTimeout(CytoscapeGraph.mouseInTimeout);
-          CytoscapeGraph.mouseInTimeout = null;
-        }
-        if (CytoscapeGraph.mouseOutTimeout) {
-          clearTimeout(CytoscapeGraph.mouseOutTimeout);
-          CytoscapeGraph.mouseOutTimeout = null;
-        }
-
-        if (tapped === CytoscapeGraph.tapTarget) {
-          // if we click the same target again, perform double-tap
-          tapped = null;
-          CytoscapeGraph.tapTarget = null;
-          const cytoscapeEvent = getCytoscapeBaseEvent(event);
-          if (cytoscapeEvent) {
-            this.handleDoubleTap(cytoscapeEvent);
-          }
-        }
-      }
-      if (tapped) {
-        // start single-tap timer
-        CytoscapeGraph.tapTarget = tapped;
-        CytoscapeGraph.tapTimeout = setTimeout(() => {
-          // timer expired without a follow-up click, so perform single-tap
-          CytoscapeGraph.tapTarget = null;
-          const cytoscapeEvent = getCytoscapeBaseEvent(event);
-          if (cytoscapeEvent) {
-            // ignore if clicking the graph background and this is not the main graph
-            if (
-              cytoscapeEvent.summaryType === 'graph' &&
-              (this.props.isMiniGraph || this.props.graphData.fetchParams.node)
-            ) {
-              return;
-            }
-
-            // if clicking the same target, then unselect it (by re-selecting the graph)
-            if (
-              this.props.summaryData &&
-              cytoscapeEvent.summaryType !== 'graph' &&
-              cytoscapeEvent.summaryType === this.props.summaryData.summaryType &&
-              cytoscapeEvent.summaryTarget === this.props.summaryData.summaryTarget
-            ) {
-              this.handleTap({ summaryType: 'graph', summaryTarget: cy } as SummaryData);
-              this.selectTarget(cy);
-            } else {
-              this.handleTap(cytoscapeEvent);
-              this.selectTarget(event.target);
-            }
-          }
-        }, CytoscapeGraph.doubleTapMs);
-      }
-    });
-
-    // Note that at the time of writing (on my chrome) the order of box events:
-    // 1) boxstart
-    // 2) boxend
-    // 3) box, a separate event for each boxselected element
-    // The boxselect event never seems to fire. boxend does not seem to supply the boxselected collection (why?).
-    // So, boxend seems not useful. I don't see a way to do this other than to 'fit' each time we add an elem.
-    cy.on('boxstart', (evt: Cy.EventObject) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-      if (cytoscapeEvent) {
-        this.userBoxSelected = cy.collection();
-      }
-    });
-
-    cy.on('box', (evt: Cy.EventObject) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-      if (cytoscapeEvent) {
-        const elements: Cy.Collection = evt.target;
-        if (elements) {
-          elements.forEach(e => {
-            if (e.data(NodeAttr.nodeType) !== NodeType.BOX) {
-              this.userBoxSelected = this.userBoxSelected?.add(elements);
-            }
-          });
-          CytoscapeGraphUtils.safeFit(cy, this.userBoxSelected);
-          this.customViewport = true;
-        }
-      }
-    });
-
-    cy.on('mouseover', 'node,edge', (evt: Cy.EventObject) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-      if (!cytoscapeEvent) {
-        return;
-      }
-
-      // cancel any active mouseOut timer
-      if (CytoscapeGraph.mouseOutTimeout) {
-        clearTimeout(CytoscapeGraph.mouseOutTimeout);
-        CytoscapeGraph.mouseOutTimeout = null;
-      }
-
-      // start mouseIn timer
-      CytoscapeGraph.mouseInTimeout = setTimeout(() => {
-        // timer expired without a mouseout so perform highlighting and show hover contextInfo
-        this.handleMouseIn(cytoscapeEvent);
-
-        // if we are not showing labels (due to zoom level), show contextInfo
-        const zoom = cy.zoom();
-        const noLabels = this.zoomThresholds.some(zoomThresh => {
-          return zoom <= zoomThresh;
-        });
-        if (noLabels) {
-          this.contextMenuRef!.current!.handleContextMenu(cytoscapeEvent.summaryTarget, true);
-        }
-      }, CytoscapeGraph.hoverInMs);
-
-      // change to mouse pointer for consistency in UI
-      const container = cy.container();
-      if (container !== null) {
-        container.style.cursor = 'pointer';
-      }
-    });
-
-    cy.on('mouseout', 'node,edge', (evt: Cy.EventObject) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-
-      if (!cytoscapeEvent) {
-        return;
-      }
-
-      // cancel any active mouseIn timer
-      if (CytoscapeGraph.mouseInTimeout) {
-        clearTimeout(CytoscapeGraph.mouseInTimeout);
-        CytoscapeGraph.mouseInTimeout = null;
-      }
-
-      // start mouseOut timer
-      CytoscapeGraph.mouseOutTimeout = setTimeout(() => {
-        // timer expired so remove contextInfo
-        this.contextMenuRef!.current!.hideContextMenu(true);
-      }, CytoscapeGraph.hoverOutMs);
-
-      // remove highlighting
-      this.handleMouseOut(cytoscapeEvent);
-
-      // change to mouse pointer back to default, for consistency in UI
-      const container = cy.container();
-      if (container !== null) {
-        container.style.cursor = 'default';
-      }
-    });
-
-    cy.on('viewport', (evt: Cy.EventObject) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-      if (cytoscapeEvent) {
-        this.customViewport = true;
-      }
-    });
-
-    // 'kiali-fit' is a custom event that we emit allowing us to reset cytoscapeGraph.customViewport
-    cy.on('kiali-fit', (evt: Cy.EventObject) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-      if (cytoscapeEvent) {
-        this.customViewport = false;
-      }
-    });
-
-    // 'kiali-zoomignore' is a custom event that we emit before and after a graph manipulation
-    // that can generate unwanted 'intermediate' values (like a CytsoscapeGraphUtils.runLayout()).
-    // note - this event does not currently support nesting (i.e. expects true followed by false)
-    cy.on('kiali-zoomignore', (evt: Cy.EventObject, zoomIgnore: boolean) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-      if (cytoscapeEvent) {
-        // When ending the zoomIgnore update to the current zoom level to prepare for the next 'zoom' event
-        if (!zoomIgnore) {
-          this.zoom = cy.zoom();
-        }
-        this.zoomIgnore = zoomIgnore;
-      }
-    });
-
-    // Crossing a zoom threshold can affect labeling, and so we refresh labels when crossing a threshold.
-    // Some cy 'zoom' events need to be ignored, typically while a layout or drag-zoom 'box' event is
-    // in progress, as cy can generate unwanted 'intermediate' values.  So check for zoomIgnore=true.
-    cy.on('zoom', (evt: Cy.EventObject) => {
-      const cytoscapeEvent = getCytoscapeBaseEvent(evt);
-      if (!cytoscapeEvent || this.zoomIgnore) {
-        return;
-      }
-
-      const oldZoom = this.zoom;
-      const newZoom = cy.zoom();
-      this.zoom = newZoom;
-
-      const thresholdCrossed = this.zoomThresholds.some(zoomThresh => {
-        return (newZoom < zoomThresh && oldZoom >= zoomThresh) || (newZoom >= zoomThresh && oldZoom < zoomThresh);
-      });
-
-      if (thresholdCrossed) {
-        CytoscapeGraphUtils.refreshLabels(cy, false);
-      }
-    });
-
-    // We use a 'layoutstop' even handler to perform common handling, as layouts can be initiated
-    // outside of just this class (for example, graph hide).
-    cy.on('layoutstop', (_evt: Cy.EventObject) => {
-      // re-enable zoom handling after the 'fit' to avoid any chance of a zoom-threshold-cross loop
-      cy.emit('kiali-zoomignore', [false]);
-
-      // After a layout Cytoscape seems to occasionally use stale visibility and/or positioning for labels.
-      // It looks like a cy bug to me, but maybe it has to do with either the html node-label extension,
-      // or our BoxLayout.  Anyway, refreshing them here seems to usually fix the issue.
-      CytoscapeGraphUtils.refreshLabels(cy, false);
-
-      // Perform a safeFit (one that takes into consideration a custom viewport set by the user).  This will
-      // ensure we limit to max-zoom, or fit to the viewport when appropriate.
-      this.safeFit(cy);
-
-      // Finally, massage any loop edges as best as possible
-      this.fixLoopOverlap(cy);
-    });
-
-    cy.on('nodehtml-create-or-update', 'node', (evt: Cy.EventObjectNode, data: any) => {
-      const { label, isNew } = data;
-      const { target } = evt;
-      // This is the DOM node of the label, if we want the cyNode it is `target`
-      const node = label.getNode();
-
-      // Assign to the label node (the DOM element) an id that matches the cy node.
-      // This is so that when we click, we can identify if the clicked label belongs to
-      // any cy node and select it
-      // Note that we don't add an actual listener to this DOM node. We use the cy click event, this proved to be more
-      // stable than adding a listener. As we only want the contents to match and not the whole node (which is bigger).
-      if (isNew) {
-        node.setAttribute('data-node-id', target.id());
-      }
-
-      // Skip root nodes from bounding expansion calculation, their size is defined by their contents, so no point in
-      // messing with these values.
-      if (target.isParent() && !target.isChild()) {
-        return;
-      }
-
-      // The code below expands the bounds of a node, taking into consideration the labels. This is important not only
-      // for displaying the label, but to avoid nodes overlapping with other labels.
-      // We assume that a label is placed centered in the bottom part of a node.
-      // The algorithm is:
-      // - Take the old bounds-expansion
-      // - Get the bounding-box of a node (without taking into account the overlays  i.e. the one that appears on click)
-      // - Compute the required extra width as the label width minus the bounding box width
-      //   - This will yield a a positive number if we need more space, or negative if we need less space.
-      // - Compute the required height as the height of the label. Since the label is at the bottom, we only need that.
-      //   If its center was aligned with the center of the node, we would do a similar operation as with the width.
-      // - Spread the required width as extra space in the left area and space in the right area of the cy node
-      //   (half in each side)
-      // - Required height is only needed at the bottom, so we know that we always have to grow at the bottom by this value.
-
-      let oldBE = target.numericStyle('bounds-expansion');
-      if (oldBE.length === 1) {
-        oldBE = Array(4).fill(oldBE[0]);
-      }
-      // Do not include the "click" overlay on the bounding box calc
-      const bb = target.boundingBox({ includeOverlays: false });
-      let newBE = [...oldBE];
-      const requiredWidth = node.offsetWidth - bb.w;
-      const requiredHeight = node.offsetHeight;
-      newBE[1] += requiredWidth * 0.5;
-      newBE[3] += requiredWidth * 0.5;
-      newBE[2] = requiredHeight;
-
-      // Ensure we don't end with negative values in our bounds-expansion
-      newBE = newBE.map(val => Math.max(val, 0));
-
-      const compareBoundsExpansion = (be1: number[], be2: number[]) => {
-        if (be1.length !== be2.length) {
-          return false;
-        }
-
-        const delta = 0.00001;
-
-        for (let i = 0; i < be1.length; ++i) {
-          if (Math.abs(be1[i] - be2[i]) > delta) {
-            return false;
-          }
-        }
-        return true;
-      };
-
-      // Only trigger an update if it really changed, else just skip to avoid this function to call again
-      if (!compareBoundsExpansion(oldBE, newBE)) {
-        target.style('bounds-expansion', newBE);
-      }
-    });
-
-    cy.ready((evt: Cy.EventObject) => {
-      if (this.props.onReady) {
-        this.props.onReady(evt.cy);
-      }
-      this.needsInitialLayout = true;
-    });
-
-    cy.on('destroy', (_evt: Cy.EventObject) => {
-      this.trafficRenderer!.stop();
-      this.trafficRenderer = undefined;
-      this.cy = undefined;
-      if (this.props.updateSummary) {
-        this.props.updateSummary({ summaryType: 'graph', summaryTarget: undefined });
-      }
-    });
-  }
-
-  private focus(cy: Cy.Core) {
-    if (!this.focusSelector) {
-      return;
-    }
-
-    // only perform the focus one time
-    const focusSelector = this.focusSelector;
-    this.focusSelector = undefined;
-
-    let selected = cy.$(focusSelector);
-
-    if (!selected) {
-      addInfo(
-        'Could not focus on requested node. The node may be idle or hidden.',
-        true,
-        undefined,
-        `${focusSelector}`
-      );
-      return;
-    }
-
-    // If there is only one, select it
-    if (selected.length === 1) {
-      this.selectTargetAndUpdateSummary(selected[0]);
-    } else {
-      // If we have many elements, try to check if a compound in this query contains everything, if so, select it.
-      const compound = selected.filter('$node > node');
-      if (compound && compound.length === 1 && selected.subtract(compound).same(compound.children())) {
-        this.selectTargetAndUpdateSummary(compound[0]);
-        selected = compound;
-      }
-    }
-
-    // Start animation
-    new FocusAnimation(cy).start(selected);
-  }
-
-  private safeFit(cy: Cy.Core, force?: boolean) {
-    if (!force && this.customViewport) {
-      return;
-    }
-
-    CytoscapeGraphUtils.safeFit(cy);
-    this.focus(cy);
-  }
-
-  private processGraphUpdate(cy: Cy.Core, runLayout: boolean, newLayout: boolean): Promise<void> {
-    this.trafficRenderer!.pause();
-
-    const isTheGraphSelected = cy.$(':selected').length === 0;
-
-    const globalScratchData: CytoscapeGlobalScratchData = {
-      activeNamespaces: this.props.graphData.fetchParams.namespaces,
-      edgeLabels: this.props.edgeLabels,
-      forceLabels: false,
-      graphType: this.props.graphData.fetchParams.graphType,
-      homeCluster: homeCluster?.name || CLUSTER_DEFAULT,
-      showOutOfMesh: this.props.showOutOfMesh,
-      showSecurity: this.props.showSecurity,
-      showVirtualServices: this.props.showVirtualServices,
-      trafficRates: this.props.graphData.fetchParams.trafficRates
-    };
-    cy.scratch(CytoscapeGlobalScratchNamespace, globalScratchData);
-
-    let elements = this.props.graphData.elements;
-    let scoringCriteria: ScoringCriteria[] = [];
-    if (this.props.showRank) {
-      for (const ranking of this.props.rankBy) {
-        if (ranking === RankMode.RANK_BY_INBOUND_EDGES) {
-          scoringCriteria.push(ScoringCriteria.InboundEdges);
-        }
-        if (ranking === RankMode.RANK_BY_OUTBOUND_EDGES) {
-          scoringCriteria.push(ScoringCriteria.OutboundEdges);
-        }
-      }
-
-      let upperBound = 0;
-      ({ elements, upperBound } = scoreNodes(this.props.graphData.elements, ...scoringCriteria));
-      if (this.props.setRankResult) {
-        this.props.setRankResult({ upperBound });
-      }
-    } else {
-      scoreNodes(this.props.graphData.elements, ...scoringCriteria);
-    }
-
-    // don't preserve any user pan/zoom when completely changing the layout
-    if (newLayout) {
-      this.customViewport = false;
-    }
-
-    cy.startBatch();
-
-    // KIALI-1291 issue was caused because some layouts (can't tell if all) do reuse the existing positions.
-    // We got some issues when changing from/to cola/cose, as the nodes started to get far away from each other.
-    // Previously we deleted the nodes prior to a layout update, this was too much and it seems that only resetting the
-    // positions to 0,0 makes the layout more predictable.
-    if (runLayout) {
-      cy.nodes().positions({ x: 0, y: 0 });
-    }
-
-    // update the entire set of nodes and edges to keep the graph up-to-date
-    cy.json({ elements: elements });
-
-    cy.endBatch();
-
-    // Compute edge healths one time for the graph
-    assignEdgeHealth(cy);
-
-    // Run layout outside of the batch operation for it to take effect on the new nodes,
-    // Layouts can run async so wait until it completes to finish the graph update.
-    if (runLayout) {
-      return new Promise((resolve, _reject) => {
-        CytoscapeGraphUtils.runLayout(cy, this.props.layout, this.props.namespaceLayout).then(_response => {
-          this.finishGraphUpdate(cy, isTheGraphSelected, runLayout);
-          resolve();
-        });
-      });
-    } else {
-      this.finishGraphUpdate(cy, isTheGraphSelected, runLayout);
-      return Promise.resolve();
-    }
-  }
-
-  private finishGraphUpdate(cy: Cy.Core, isTheGraphSelected: boolean, runLayout: boolean) {
-    // We opt-in for manual selection to be able to control when to select a node/edge
-    // https://github.com/cytoscape/cytoscape.js/issues/1145#issuecomment-153083828
-    cy.nodes().unselectify();
-    cy.edges().unselectify();
-
-    // Verify our current selection is still valid, if not, select the graph
-    if (!isTheGraphSelected && cy.$(':selected').length === 0) {
-      this.handleTap({ summaryType: 'graph', summaryTarget: cy });
-    }
-
-    // When the graphUpdate runs a layout then this logic is handled in the 'layoutstop' eventhandler, otherwise do it here
-    if (!runLayout) {
-      CytoscapeGraphUtils.refreshLabels(cy, false);
-      cy.emit('kiali-zoomignore', [false]);
-    }
-
-    if (this.props.showTrafficAnimation) {
-      this.trafficRenderer!.start(cy.edges());
-    }
-
-    // notify that the graph has been updated
-    if (this.props.setUpdateTime) {
-      this.props.setUpdateTime(Date.now());
-    }
-  }
-
-  private selectTarget = (target?: Cy.NodeSingular | Cy.EdgeSingular | Cy.Core) => {
-    if (this.cy) {
-      this.cy.$(':selected').selectify().unselect().unselectify();
-      if (target && !isCore(target)) {
-        target.selectify().select().unselectify();
-      }
-    }
-  };
-
-  private selectTargetAndUpdateSummary = (target: Cy.NodeSingular | Cy.EdgeSingular) => {
-    this.selectTarget(target);
-    const event: GraphEvent = {
-      summaryType: target.data(NodeAttr.isBox) ? 'box' : 'node',
-      summaryTarget: target
-    };
-    if (this.props.updateSummary) {
-      this.props.updateSummary(event);
-    }
-    this.graphHighlighter!.onClick(event);
-  };
-
-  private handleDoubleTap = (event: GraphEvent) => {
-    if (this.props.onNodeDoubleTap && CytoscapeGraph.isCyNodeClickEvent(event)) {
-      this.props.onNodeDoubleTap(CytoscapeGraph.buildTapEventArgs(event) as GraphNodeTapEvent);
-    }
-  };
-
-  private handleTap = (event: GraphEvent) => {
-    if (this.props.updateSummary) {
-      this.props.updateSummary(event);
-    }
-
-    if (this.props.onNodeTap && CytoscapeGraph.isCyNodeClickEvent(event)) {
-      this.props.onNodeTap(CytoscapeGraph.buildTapEventArgs(event) as GraphNodeTapEvent);
-    }
-
-    if (!this.props.isMiniGraph) {
-      this.graphHighlighter!.onClick(event);
-    } else if (this.props.onEdgeTap && CytoscapeGraph.isCyEdgeClickEvent(event)) {
-      this.props.onEdgeTap(CytoscapeGraph.buildTapEventArgs(event) as GraphEdgeTapEvent);
-    }
-  };
-
-  private handleMouseIn = (event: GraphEvent) => {
-    this.graphHighlighter!.onMouseIn(event);
-  };
-
-  private handleMouseOut = (event: GraphEvent) => {
-    this.graphHighlighter!.onMouseOut(event);
-  };
-
-  private nodeNeedsRelayout() {
-    const needsRelayout = this.nodeChanged;
-    if (needsRelayout) {
-      this.nodeChanged = false;
-    }
-    return needsRelayout;
-  }
-
-  static isCyNodeClickEvent(event: GraphEvent): boolean {
-    const targetType = event.summaryType;
-    if (targetType !== 'node' && targetType !== 'box') {
-      return false;
-    }
-
-    return true;
-  }
-
-  static isCyEdgeClickEvent(event: GraphEvent): boolean {
-    const targetType = event.summaryType;
-    return targetType === 'edge';
-  }
-
-  private fixLoopOverlap(cy: Cy.Core) {
-    cy.$(':loop').forEach(loop => {
-      const node = loop.source();
-      const otherEdges = node.connectedEdges().subtract(loop);
-      const minDistance = 1;
-
-      // Default values in rads (taken from cytoscape docs)
-      const DEFAULT_LOOP_SWEEP = -1.5707;
-      const DEFAULT_LOOP_DIRECTION = -0.7854;
-
-      loop.style('loop-direction', DEFAULT_LOOP_DIRECTION);
-      loop.style('loop-sweep', DEFAULT_LOOP_SWEEP);
-
-      let found = false;
-      // Check if we have any other edge that overlaps with any of our loop edges
-      // this uses cytoscape forEach (https://js.cytoscape.org/#eles.forEach)
-      otherEdges.forEach(edge => {
-        const testPoint = edge.source().same(node) ? edge.sourceEndpoint() : edge.targetEndpoint();
-        if (
-          squaredDistance(testPoint, loop.sourceEndpoint()) <= minDistance ||
-          squaredDistance(testPoint, loop.targetEndpoint()) <= minDistance
-        ) {
-          found = true;
-          return false; // break the inner cytoscape forEach
-        }
-        return; // return to avoid typescript error about "not all code paths return a value"
-      });
-
-      if (!found) {
-        return;
-      }
-
-      // Simple case, one other edge, just move the loop-direction half the default loop-sweep value to avoid the edge
-      if (otherEdges.length === 1) {
-        const loopDirection = loop.numericStyle('loop-direction') - loop.numericStyle('loop-sweep') * 0.5;
-        loop.style('loop-direction', loopDirection);
-        return;
-      }
-
-      // Compute every angle between the top (12 o’clock position)
-      // We store the angles as radians and positive numbers, thus we add PI to the negative angles.
-      const usedAngles: number[] = [];
-      otherEdges.forEach(edge => {
-        const testPoint = edge.source().same(node) ? edge.sourceEndpoint() : edge.targetEndpoint();
-        const angle = angleBetweenVectors(
-          normalize({ x: testPoint.x - node.position().x, y: testPoint.y - node.position().y }),
-          { x: 0, y: 1 }
-        );
-        usedAngles.push(angle < 0 ? angle + 2 * Math.PI : angle);
-      });
-
-      usedAngles.sort((a, b) => a - b);
-
-      // Try to fit our loop in the longest arc
-      // Iterate over the found angles and find the longest distance
-      let maxArc = {
-        start: 0,
-        end: 0,
-        value: 0
-      };
-      for (let i = 0; i < usedAngles.length; ++i) {
-        const start = i === 0 ? usedAngles[usedAngles.length - 1] : usedAngles[i - 1];
-        const end = usedAngles[i];
-        const arc = Math.abs(start - end);
-        if (arc > maxArc.value) {
-          maxArc.value = arc;
-          maxArc.start = start;
-          maxArc.end = end;
-        }
-      }
-
-      // If the max arc is 1.0 radians (the biggest gap is of about 50 deg), the node is already too busy, ignore it
-      if (maxArc.value < 1.0) {
-        return;
-      }
-
-      if (maxArc.start > maxArc.end) {
-        // To ensure the difference between end and start goes in the way we want, we add a full circle to our end
-        maxArc.end += Math.PI * 2;
-      }
-
-      if (maxArc.value <= -DEFAULT_LOOP_SWEEP) {
-        // Make it slightly smaller to be able to fit
-        // loop-sweep is related to the distance between the start and end of our loop edge
-        loop.style('loop-sweep', -maxArc.value * 0.9);
-        maxArc.start += maxArc.value * 0.05;
-        maxArc.end -= maxArc.value * 0.05;
-      }
-      // Move the loop to the center of the arc, loop-direction is related to the middle point of the loop
-      loop.style('loop-direction', maxArc.start + (maxArc.end - maxArc.start) * 0.5);
-    });
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeGraphUtils.ts b/frontend/src/components/CytoscapeGraph/CytoscapeGraphUtils.ts
deleted file mode 100644
index 199348f0d..000000000
--- a/frontend/src/components/CytoscapeGraph/CytoscapeGraphUtils.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import * as LayoutDictionary from './graphs/LayoutDictionary';
-import {
-  CytoscapeGlobalScratchData,
-  CytoscapeGlobalScratchNamespace,
-  DecoratedGraphEdgeData,
-  DecoratedGraphEdgeWrapper,
-  DecoratedGraphElements,
-  DecoratedGraphNodeData,
-  DecoratedGraphNodeWrapper,
-  Layout
-} from '../../types/Graph';
-import * as Cy from 'cytoscape';
-import { GraphStyles } from './graphs/GraphStyles';
-
-export const ZoomOptions = {
-  fitPadding: 40,
-  maxZoom: 2.5
-};
-
-export const safeFit = (cy: Cy.Core, centerElements?: Cy.Collection) => {
-  cy.fit(centerElements, ZoomOptions.fitPadding);
-  if (cy.zoom() > ZoomOptions.maxZoom) {
-    cy.zoom(ZoomOptions.maxZoom);
-    !!centerElements && !!centerElements.length ? cy.center(centerElements) : cy.center();
-  }
-  // 'kiali-fit' is a custom event that we emit allowing us to reset cytoscapeGraph.customViewport
-  cy.emit('kiali-fit');
-};
-
-// IMPORTANT! Layouts should be performed while zoom-handling is being ignored:
-//   - call cy.emit('kiali-zoomignore', [true]) at some point prior to this call
-//   - call cy.emit('kiali-zoomignore', [false]) in the promise handler
-export const runLayout = (cy: Cy.Core, layout: Layout, namespaceLayout: Layout): Promise<any> => {
-  // generate all labels so the layout algorithm can take them into consideration
-  refreshLabels(cy, true);
-
-  const layoutOptions = LayoutDictionary.getLayout(layout);
-  let promise: Promise<any>;
-  let cyLayout: Cy.Layouts;
-  if (cy.nodes('$node > node').length > 0) {
-    // if there is any parent (i.e. box) node, run the box-layout
-    cyLayout = cy.layout({
-      ...layoutOptions,
-      name: 'box-layout',
-      appBoxLayout: namespaceLayout.name, // app and namespace will share same layout
-      namespaceBoxLayout: namespaceLayout.name,
-      defaultLayout: layout.name
-    });
-  } else {
-    cyLayout = cy.layout(layoutOptions);
-  }
-  promise = cyLayout.promiseOn('layoutstop');
-  cyLayout.run();
-  return promise;
-};
-
-// This should be called to ensure labels are up to date on-screen.  It may be needed to ensure cytoscape
-// displays up-to-date labels, even if the label content has not changed.
-// Note: the leaf-to-root approach here should mirror what is done in GraphStyles.ts#htmlNodeLabels()
-export const refreshLabels = (cy: Cy.Core, force: boolean) => {
-  const scratch = cy.scratch(CytoscapeGlobalScratchNamespace);
-  if (force) {
-    if (scratch) {
-      cy.scratch(CytoscapeGlobalScratchNamespace, { ...scratch, forceLabels: true } as CytoscapeGlobalScratchData);
-    }
-  }
-
-  // update labels from leaf to node (i.e. inside out with respect to nested boxing).  This (in theory) ensures
-  // that outer nodes will always be able to incorporate inner nodes' true bounding-box (one adjusted for the label).
-  let nodes = cy.nodes('[^isBox]:visible');
-  while (nodes.length > 0) {
-    (cy as any).nodeHtmlLabel().updateNodeLabel(nodes);
-    nodes = nodes.parents();
-  }
-
-  cy.edges().each(e => {
-    e.data('label', GraphStyles.getEdgeLabel(e, e.selected()));
-  });
-
-  if (force) {
-    if (scratch) {
-      cy.scratch(CytoscapeGlobalScratchNamespace, { ...scratch, forceLabels: false } as CytoscapeGlobalScratchData);
-    }
-  }
-};
-
-// It is common that when updating the graph that the element topology (nodes, edges) remain the same,
-// only their activity changes (rates, etc). When the topology remains the same we may be able to optimize
-// some behavior.  This returns true if the topology changes, false otherwise.
-// 1) Quickly compare the number of nodes and edges, if different return true.
-// 2) Compare the ids
-export const elementsChanged = (
-  prevElements: DecoratedGraphElements,
-  nextElements: DecoratedGraphElements
-): boolean => {
-  if (prevElements === nextElements) {
-    return false;
-  }
-
-  if (
-    !prevElements ||
-    !nextElements ||
-    !prevElements.nodes ||
-    !prevElements.edges ||
-    !nextElements.nodes ||
-    !nextElements.edges ||
-    prevElements.nodes.length !== nextElements.nodes.length ||
-    prevElements.edges.length !== nextElements.edges.length
-  ) {
-    return true;
-  }
-
-  return !(
-    nodeOrEdgeArrayHasSameIds(nextElements.nodes, prevElements.nodes) &&
-    nodeOrEdgeArrayHasSameIds(nextElements.edges, prevElements.edges)
-  );
-};
-
-const nodeOrEdgeArrayHasSameIds = <T extends DecoratedGraphNodeWrapper | DecoratedGraphEdgeWrapper>(
-  a: Array<T>,
-  b: Array<T>
-): boolean => {
-  const aIds = a.map(e => e.data.id).sort();
-  return b
-    .map(e => e.data.id)
-    .sort()
-    .every((eId, index) => eId === aIds[index]);
-};
-
-export const decoratedEdgeData = (ele: Cy.EdgeSingular): DecoratedGraphEdgeData => {
-  return ele.data();
-};
-
-export const decoratedNodeData = (ele: Cy.NodeSingular): DecoratedGraphNodeData => {
-  return ele.data();
-};
-
-export const isCore = (target: Cy.NodeSingular | Cy.EdgeSingular | Cy.Core): target is Cy.Core => {
-  return !('cy' in target);
-};
-
-export const isNode = (target: Cy.NodeSingular | Cy.EdgeSingular | Cy.Core): target is Cy.NodeSingular => {
-  return !isCore(target) && target.isNode();
-};
-
-export const isEdge = (target: Cy.NodeSingular | Cy.EdgeSingular | Cy.Core): target is Cy.EdgeSingular => {
-  return !isCore(target) && target.isEdge();
-};
-
-export const toSafeCyFieldName = (fieldName: string): string => {
-  const alnumString = /^[a-zA-Z0-9]*$/;
-  const unsafeChar = /[^a-zA-Z0-9]/g;
-
-  if (fieldName.match(alnumString)) {
-    return fieldName;
-  }
-
-  return fieldName.replace(unsafeChar, '_');
-};
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeReactWrapper.tsx b/frontend/src/components/CytoscapeGraph/CytoscapeReactWrapper.tsx
deleted file mode 100644
index 510fbb4ed..000000000
--- a/frontend/src/components/CytoscapeGraph/CytoscapeReactWrapper.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import * as React from 'react';
-import * as Cy from 'cytoscape';
-
-import { GraphStyles } from './graphs/GraphStyles';
-import canvas from 'cytoscape-canvas';
-import cytoscape from 'cytoscape';
-import { BoxLayout } from './Layout/BoxLayout';
-import popper from 'cytoscape-popper';
-import { KialiGridLayout } from './Layout/KialiGridLayout';
-import { KialiDagreLayout } from './Layout/KialiDagreLayout';
-import { KialiConcentricLayout } from './Layout/KialiConcentricLayout';
-import { KialiBreadFirstLayout } from './Layout/KialiBreadthFirstLayout';
-import { setPFColorVals } from 'components/Pf/PfColors';
-const nodeHtmlLabel = require('cy-node-html-label');
-
-cytoscape.use(canvas);
-cytoscape.use(popper);
-cytoscape('layout', 'box-layout', BoxLayout);
-cytoscape('layout', 'kiali-breadthfirst', KialiBreadFirstLayout);
-cytoscape('layout', 'kiali-dagre', KialiDagreLayout);
-cytoscape('layout', 'kiali-grid', KialiGridLayout);
-cytoscape('layout', 'kiali-concentric', KialiConcentricLayout);
-
-nodeHtmlLabel(cytoscape);
-
-type CytoscapeReactWrapperProps = {};
-
-type CytoscapeReactWrapperState = {};
-
-const styleContainer: React.CSSProperties = {
-  height: '100%'
-};
-
-/**
- * The purpose of this wrapper is very simple and minimal - to provide a long-lived <div> element that can be used
- * as the parent container for the cy graph (cy.container). Because cy does not provide the ability to re-parent an
- * existing graph (e.g. there is no API such as "cy.setContainer(div)"), the only way to be able to re-use a
- * graph (without re-creating and re-rendering it all the time) is to have it inside a wrapper like this one
- * that does not update/re-render itself, thus keeping the original <div> intact.
- *
- * Other than creating and initializing the cy graph, this component should do nothing else. Parent components
- * should get a ref to this component can call getCy() in order to perform additional processing on the graph.
- * It is the job of the parent component to manipulate and update the cy graph during runtime.
- *
- * NOTE: The context menu stuff is defined in the CytoscapeReactWrapper because that is
- * where the cytoscape plugins are defined. And the context menu functions are defined in
- * here because they are not normal Cytoscape defined functions like those found in CytoscapeGraph.
- */
-export class CytoscapeReactWrapper extends React.Component<CytoscapeReactWrapperProps, CytoscapeReactWrapperState> {
-  cy?: Cy.Core;
-  divParentRef: React.RefObject<HTMLDivElement>;
-
-  constructor(props: CytoscapeReactWrapperProps) {
-    super(props);
-    this.divParentRef = React.createRef();
-  }
-
-  // For other components to be able to manipulate the cy graph.
-  getCy() {
-    return this.cy;
-  }
-
-  // This is VERY important - this must always return false to ensure the div is never destroyed.
-  // If the div is destroyed, the cached cy becomes useless.
-  shouldComponentUpdate(_nextProps: CytoscapeReactWrapperProps, _nextState: CytoscapeReactWrapperState) {
-    return false;
-  }
-
-  componentDidMount() {
-    this.build();
-  }
-
-  componentWillUnmount() {
-    this.destroy();
-  }
-
-  render() {
-    return <div id="cy" className="graph" style={styleContainer} ref={this.divParentRef} />;
-  }
-
-  private build() {
-    if (this.cy) {
-      this.destroy();
-    }
-
-    // ensure required colors are set
-    setPFColorVals(this.divParentRef.current!);
-
-    const opts = {
-      container: this.divParentRef.current,
-      boxSelectionEnabled: false,
-      style: GraphStyles.styles(),
-      ...GraphStyles.options()
-    };
-
-    this.cy = cytoscape(opts);
-    // Using an extension
-    (this.cy as any).nodeHtmlLabel(GraphStyles.htmlNodeLabels(this.cy));
-  }
-
-  private destroy() {
-    if (this.cy) {
-      this.cy.destroy();
-      this.cy = undefined;
-    }
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeToolbar.tsx b/frontend/src/components/CytoscapeGraph/CytoscapeToolbar.tsx
deleted file mode 100644
index ec3cec4ac..000000000
--- a/frontend/src/components/CytoscapeGraph/CytoscapeToolbar.tsx
+++ /dev/null
@@ -1,394 +0,0 @@
-import * as React from 'react';
-import * as Cy from 'cytoscape';
-import { Button, ButtonVariant, Tooltip, TooltipPosition } from '@patternfly/react-core';
-import { kialiStyle } from 'styles/StyleUtils';
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import { KialiDispatch } from 'types/Redux';
-import { KialiAppState } from '../../store/Store';
-import { PFColors } from '../Pf/PfColors';
-import * as CytoscapeGraphUtils from './CytoscapeGraphUtils';
-import { EdgeMode, Layout } from '../../types/Graph';
-import { GraphActions } from '../../actions/GraphActions';
-import { HistoryManager, URLParam } from '../../app/History';
-import * as LayoutDictionary from './graphs/LayoutDictionary';
-import { GraphToolbarActions } from '../../actions/GraphToolbarActions';
-import { GraphTourStops } from 'pages/Graph/GraphHelpTour';
-import { TourStop } from 'components/Tour/TourStop';
-import { edgeModeSelector } from 'store/Selectors';
-import { KialiDagreGraph } from './graphs/KialiDagreGraph';
-import { KialiGridGraph } from './graphs/KialiGridGraph';
-import { KialiConcentricGraph } from './graphs/KialiConcentricGraph';
-import { KialiBreadthFirstGraph } from './graphs/KialiBreadthFirstGraph';
-import { KialiIcon } from 'config/KialiIcon';
-
-type ReduxStateProps = {
-  boxByNamespace: boolean;
-  edgeMode: EdgeMode;
-  layout: Layout;
-  namespaceLayout: Layout;
-  showLegend: boolean;
-};
-
-type ReduxDispatchProps = {
-  setEdgeMode: (edgeMode: EdgeMode) => void;
-  setLayout: (layout: Layout) => void;
-  setNamespaceLayout: (layout: Layout) => void;
-  toggleLegend: () => void;
-};
-
-type CytoscapeToolbarProps = ReduxStateProps &
-  ReduxDispatchProps & {
-    cytoscapeGraphRef: any;
-    disabled: boolean;
-  };
-
-type CytoscapeToolbarState = {
-  allowGrab: boolean;
-};
-
-const activeButtonStyle = kialiStyle({
-  color: PFColors.Active
-});
-
-const buttonStyle = kialiStyle({
-  backgroundColor: PFColors.BackgroundColor100,
-  margin: '0.125rem 0.25rem',
-  padding: '0.25rem 0.5rem'
-});
-
-const cyToolbarStyle = kialiStyle({
-  width: '1.25rem'
-});
-
-class CytoscapeToolbarComponent extends React.PureComponent<CytoscapeToolbarProps, CytoscapeToolbarState> {
-  constructor(props: CytoscapeToolbarProps) {
-    super(props);
-
-    // Let URL override current redux state at construction time. Update URL with unset params.
-    const urlLayout = HistoryManager.getParam(URLParam.GRAPH_LAYOUT);
-
-    if (urlLayout) {
-      if (urlLayout !== props.layout.name) {
-        props.setLayout(LayoutDictionary.getLayoutByName(urlLayout));
-      }
-    } else {
-      HistoryManager.setParam(URLParam.GRAPH_LAYOUT, props.layout.name);
-    }
-
-    const urlNamespaceLayout = HistoryManager.getParam(URLParam.GRAPH_NAMESPACE_LAYOUT);
-
-    if (urlNamespaceLayout) {
-      if (urlNamespaceLayout !== props.namespaceLayout.name) {
-        props.setNamespaceLayout(LayoutDictionary.getLayoutByName(urlNamespaceLayout));
-      }
-    } else {
-      HistoryManager.setParam(URLParam.GRAPH_NAMESPACE_LAYOUT, props.namespaceLayout.name);
-    }
-
-    this.state = { allowGrab: false };
-  }
-
-  componentDidMount(): void {
-    // Toggle drag once when component is initialized
-    this.toggleDrag();
-  }
-
-  componentDidUpdate(): void {
-    // ensure redux state and URL are aligned
-    HistoryManager.setParam(URLParam.GRAPH_LAYOUT, this.props.layout.name);
-    HistoryManager.setParam(URLParam.GRAPH_NAMESPACE_LAYOUT, this.props.namespaceLayout.name);
-  }
-
-  render(): React.ReactNode {
-    return (
-      <div className={cyToolbarStyle}>
-        <div>
-          <Tooltip content={this.state.allowGrab ? 'Disable Drag' : 'Enable Drag'} position={TooltipPosition.right}>
-            <Button
-              id="toolbar_grab"
-              aria-label="Toggle Drag"
-              isActive={this.state.allowGrab}
-              onClick={() => this.toggleDrag()}
-              className={buttonStyle}
-              variant={ButtonVariant.plain}
-            >
-              <KialiIcon.DragDrop className={this.state.allowGrab ? activeButtonStyle : undefined} />
-            </Button>
-          </Tooltip>
-        </div>
-
-        <div>
-          <Tooltip content="Zoom to Fit" position={TooltipPosition.right}>
-            <Button
-              id="toolbar_graph_fit"
-              aria-label="Zoom to Fit"
-              onClick={() => this.fit()}
-              className={buttonStyle}
-              variant={ButtonVariant.plain}
-            >
-              <KialiIcon.ExpandArrows />
-            </Button>
-          </Tooltip>
-        </div>
-
-        <div>
-          <Tooltip content="Hide healthy edges" position={TooltipPosition.right}>
-            <Button
-              id="toolbar_edge_mode_unhealthy"
-              aria-label="Hide Healthy Edges"
-              isActive={this.props.edgeMode === EdgeMode.UNHEALTHY}
-              onClick={() => {
-                this.handleEdgeModeClick(EdgeMode.UNHEALTHY);
-              }}
-              className={buttonStyle}
-              variant={ButtonVariant.plain}
-            >
-              <KialiIcon.LongArrowRight
-                className={this.props.edgeMode === EdgeMode.UNHEALTHY ? activeButtonStyle : undefined}
-              />
-            </Button>
-          </Tooltip>
-        </div>
-
-        <div>
-          <Tooltip content="Hide all edges" position={TooltipPosition.right}>
-            <Button
-              id="toolbar_edge_mode_none"
-              aria-label="Hide All Edges"
-              isActive={this.props.edgeMode === EdgeMode.NONE}
-              onClick={() => {
-                this.handleEdgeModeClick(EdgeMode.NONE);
-              }}
-              className={buttonStyle}
-              variant={ButtonVariant.plain}
-            >
-              <KialiIcon.LongArrowRight
-                className={this.props.edgeMode === EdgeMode.NONE ? activeButtonStyle : undefined}
-              />
-            </Button>
-          </Tooltip>
-        </div>
-
-        <div>
-          <Tooltip content={`Layout default ${KialiDagreGraph.getLayout().name}`} position={TooltipPosition.right}>
-            <Button
-              id="toolbar_layout_default"
-              aria-label="Graph Layout Default Style"
-              isActive={this.props.layout.name === KialiDagreGraph.getLayout().name}
-              isDisabled={this.props.disabled}
-              onClick={() => {
-                this.setLayout(KialiDagreGraph.getLayout());
-              }}
-              className={buttonStyle}
-              variant={ButtonVariant.plain}
-            >
-              <KialiIcon.Topology
-                className={this.props.layout.name === KialiDagreGraph.getLayout().name ? activeButtonStyle : undefined}
-              />
-            </Button>
-          </Tooltip>
-        </div>
-
-        <TourStop info={GraphTourStops.Layout}>
-          <div>
-            <Tooltip content={`Layout 1 ${KialiGridGraph.getLayout().name}`} position={TooltipPosition.right}>
-              <Button
-                id="toolbar_layout1"
-                aria-label="Graph Layout Style 1"
-                isActive={this.props.layout.name === KialiGridGraph.getLayout().name}
-                isDisabled={this.props.disabled}
-                onClick={() => {
-                  this.setLayout(KialiGridGraph.getLayout());
-                }}
-                className={buttonStyle}
-                variant={ButtonVariant.plain}
-              >
-                <KialiIcon.Topology
-                  className={this.props.layout.name === KialiGridGraph.getLayout().name ? activeButtonStyle : undefined}
-                />
-              </Button>
-            </Tooltip>
-          </div>
-        </TourStop>
-
-        <div>
-          <Tooltip content={`Layout 2 ${KialiConcentricGraph.getLayout().name}`} position={TooltipPosition.right}>
-            <Button
-              id="toolbar_layout2"
-              aria-label="Graph Layout Style 2"
-              isActive={this.props.layout.name === KialiConcentricGraph.getLayout().name}
-              isDisabled={this.props.disabled}
-              onClick={() => {
-                this.setLayout(KialiConcentricGraph.getLayout());
-              }}
-              className={buttonStyle}
-              variant={ButtonVariant.plain}
-            >
-              <KialiIcon.Topology
-                className={
-                  this.props.layout.name === KialiConcentricGraph.getLayout().name ? activeButtonStyle : undefined
-                }
-              />
-            </Button>
-          </Tooltip>
-        </div>
-
-        <div>
-          <Tooltip content={`Layout 3 ${KialiBreadthFirstGraph.getLayout().name}`} position={TooltipPosition.right}>
-            <Button
-              id="toolbar_layout3"
-              aria-label="Graph Layout Style 3"
-              isActive={this.props.layout.name === KialiBreadthFirstGraph.getLayout().name}
-              isDisabled={this.props.disabled}
-              onClick={() => {
-                this.setLayout(KialiBreadthFirstGraph.getLayout());
-              }}
-              className={buttonStyle}
-              variant={ButtonVariant.plain}
-            >
-              <KialiIcon.Topology
-                className={
-                  this.props.layout.name === KialiBreadthFirstGraph.getLayout().name ? activeButtonStyle : undefined
-                }
-              />
-            </Button>
-          </Tooltip>
-        </div>
-
-        {this.props.boxByNamespace && (
-          <div>
-            <Tooltip
-              content={`Namespace Layout 1 ${KialiDagreGraph.getLayout().name}`}
-              position={TooltipPosition.right}
-            >
-              <Button
-                id="toolbar_namespace_layout1"
-                aria-label="Namespace Layout Style 1"
-                isActive={this.props.namespaceLayout.name === KialiDagreGraph.getLayout().name}
-                isDisabled={this.props.disabled}
-                onClick={() => {
-                  this.setNamespaceLayout(KialiDagreGraph.getLayout());
-                }}
-                className={buttonStyle}
-                variant={ButtonVariant.plain}
-              >
-                <KialiIcon.Tenant
-                  className={
-                    this.props.namespaceLayout.name === KialiDagreGraph.getLayout().name ? activeButtonStyle : undefined
-                  }
-                />
-              </Button>
-            </Tooltip>
-          </div>
-        )}
-
-        {this.props.boxByNamespace && (
-          <div>
-            <Tooltip
-              content={`Namespace Layout 2 ${KialiBreadthFirstGraph.getLayout().name}`}
-              position={TooltipPosition.right}
-            >
-              <Button
-                id="toolbar_namespace_layout2"
-                aria-label="Namespace Layout Style 2"
-                isActive={this.props.namespaceLayout.name === KialiBreadthFirstGraph.getLayout().name}
-                isDisabled={this.props.disabled}
-                onClick={() => {
-                  this.setNamespaceLayout(KialiBreadthFirstGraph.getLayout());
-                }}
-                className={buttonStyle}
-                variant={ButtonVariant.plain}
-              >
-                <KialiIcon.Tenant
-                  className={
-                    this.props.namespaceLayout.name === KialiBreadthFirstGraph.getLayout().name
-                      ? activeButtonStyle
-                      : undefined
-                  }
-                />
-              </Button>
-            </Tooltip>
-          </div>
-        )}
-
-        <TourStop info={GraphTourStops.Legend}>
-          <div>
-            <Tooltip content="Show Legend" position={TooltipPosition.right}>
-              <Button
-                id="toolbar_toggle_legend"
-                aria-label="Show Legend"
-                isActive={this.props.showLegend}
-                onClick={this.props.toggleLegend}
-                className={buttonStyle}
-                variant={ButtonVariant.plain}
-              >
-                <KialiIcon.Map className={this.props.showLegend ? activeButtonStyle : undefined} size="sm" />
-              </Button>
-            </Tooltip>
-          </div>
-        </TourStop>
-      </div>
-    );
-  }
-
-  private getCy = (): Cy.Core | null => {
-    if (this.props.cytoscapeGraphRef.current) {
-      return this.props.cytoscapeGraphRef.current.getCy();
-    }
-
-    return null;
-  };
-
-  private toggleDrag = (): void => {
-    const cy = this.getCy();
-
-    if (!cy) {
-      return;
-    }
-
-    cy.autoungrabify(this.state.allowGrab);
-    this.setState({ allowGrab: !this.state.allowGrab });
-  };
-
-  private fit = (): void => {
-    const cy = this.getCy();
-
-    if (cy) {
-      CytoscapeGraphUtils.safeFit(cy);
-    }
-  };
-
-  private handleEdgeModeClick = (edgeMode: EdgeMode): void => {
-    this.props.setEdgeMode(edgeMode === this.props.edgeMode ? EdgeMode.ALL : edgeMode);
-  };
-
-  private setLayout = (layout: Layout): void => {
-    if (layout.name !== this.props.layout.name) {
-      this.props.setLayout(layout);
-    }
-  };
-
-  private setNamespaceLayout = (layout: Layout): void => {
-    if (layout.name !== this.props.namespaceLayout.name) {
-      this.props.setNamespaceLayout(layout);
-    }
-  };
-}
-
-const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({
-  edgeMode: edgeModeSelector(state),
-  boxByNamespace: state.graph.toolbarState.boxByNamespace,
-  layout: state.graph.layout,
-  namespaceLayout: state.graph.namespaceLayout,
-  showLegend: state.graph.toolbarState.showLegend
-});
-
-const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => ({
-  setEdgeMode: bindActionCreators(GraphActions.setEdgeMode, dispatch),
-  setLayout: bindActionCreators(GraphActions.setLayout, dispatch),
-  setNamespaceLayout: bindActionCreators(GraphActions.setNamespaceLayout, dispatch),
-  toggleLegend: bindActionCreators(GraphToolbarActions.toggleLegend, dispatch)
-});
-
-export const CytoscapeToolbar = connect(mapStateToProps, mapDispatchToProps)(CytoscapeToolbarComponent);
diff --git a/frontend/src/components/CytoscapeGraph/CytoscapeTrace.ts b/frontend/src/components/CytoscapeGraph/CytoscapeTrace.ts
deleted file mode 100644
index 4a313e4ae..000000000
--- a/frontend/src/components/CytoscapeGraph/CytoscapeTrace.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import * as Cy from 'cytoscape';
-import { JaegerTrace, Span } from 'types/TracingInfo';
-import { NodeType, GraphType, SEInfo, NodeAttr } from 'types/Graph';
-import {
-  getAppFromSpan,
-  getWorkloadFromSpan,
-  searchParentApp,
-  searchParentWorkload
-} from 'utils/tracing/TracingHelper';
-
-export const showTrace = (cy: Cy.Core, graphType: GraphType, trace: JaegerTrace) => {
-  if (!cy) {
-    return;
-  }
-
-  cy.startBatch();
-  hideTrace(cy);
-  trace.spans.forEach(span => showSpanSubtrace(cy, graphType, span));
-  cy.endBatch();
-};
-
-const showSpanSubtrace = (cy: Cy.Core, graphType: GraphType, span: Span) => {
-  const split = span.process.serviceName.split('.');
-  const app = split[0];
-
-  // From upstream to downstream: Parent app or workload, Inbound Service Entry, Service, App or Workload, Outbound Service Entry
-  let lastSelection: Cy.NodeCollection | undefined = undefined;
-
-  if (graphType === GraphType.SERVICE) {
-    // In service graph type, parent can be a Service or a Workload (e.g. when it initiates the transaction)
-    const sourceAppNs = searchParentApp(span);
-    if (sourceAppNs) {
-      let selector = `node[!${NodeAttr.isBox}][${NodeAttr.nodeType}="${NodeType.SERVICE}"][${NodeAttr.app}="${sourceAppNs.app}"][${NodeAttr.namespace}="${sourceAppNs.namespace}"]`;
-      let parent = cy.elements(selector);
-      if (!parent || parent.length === 0) {
-        // Try workload
-        const sourceWlNs = searchParentWorkload(span);
-        if (sourceWlNs) {
-          selector = `node[${NodeAttr.workload}="${sourceWlNs.workload}"][${NodeAttr.namespace}="${sourceWlNs.namespace}"]`;
-          parent = cy.elements(selector);
-        }
-      }
-      if (!!parent && parent.length !== 0) {
-        lastSelection = parent;
-      }
-    }
-  } else if (graphType === GraphType.APP) {
-    // Parent app
-    const sourceAppNs = searchParentApp(span);
-    if (sourceAppNs) {
-      const selector = `node[${NodeAttr.nodeType}="${NodeType.APP}"][${NodeAttr.app}="${sourceAppNs.app}"][${NodeAttr.namespace}="${sourceAppNs.namespace}"]`;
-      const parent = cy.elements(selector);
-      if (!!parent && parent.length !== 0) {
-        lastSelection = parent;
-      }
-    }
-  } else {
-    // Parent workload
-    const sourceWlNs = searchParentWorkload(span);
-    if (sourceWlNs) {
-      const selector = `node[${NodeAttr.workload}="${sourceWlNs.workload}"][${NodeAttr.namespace}="${sourceWlNs.namespace}"]`;
-      const parent = cy.elements(selector);
-      if (!!parent && parent.length !== 0) {
-        lastSelection = parent;
-      }
-    }
-  }
-
-  // Inbound service entry
-  const seSelectionInbound = getInboundServiceEntry(span, cy);
-  lastSelection = nextHop(span, seSelectionInbound, lastSelection);
-
-  // Main service
-  const nsSelector = split.length > 1 ? `[${NodeAttr.namespace}="${split[1]}"]` : '';
-  const selector = `[${NodeAttr.nodeType}="${NodeType.SERVICE}"][${NodeAttr.app}="${app}"]${nsSelector}`;
-  lastSelection = nextHop(span, cy.elements(selector), lastSelection);
-
-  if (graphType === GraphType.APP) {
-    // Main app
-    const destAppNs = getAppFromSpan(span);
-    if (destAppNs) {
-      const selector = `node[${NodeAttr.nodeType}="${NodeType.APP}"][${NodeAttr.app}="${destAppNs.app}"][${NodeAttr.namespace}="${destAppNs.namespace}"]`;
-      lastSelection = nextHop(span, cy.elements(selector), lastSelection);
-    }
-  } else {
-    // Main workload
-    const destWlNs = getWorkloadFromSpan(span);
-    if (destWlNs) {
-      const selector = `node[${NodeAttr.workload}="${destWlNs.workload}"][${NodeAttr.namespace}="${destWlNs.namespace}"]`;
-      lastSelection = nextHop(span, cy.elements(selector), lastSelection);
-    }
-  }
-
-  // Outbound service entry
-  const seSelection = getOutboundServiceEntry(span, cy);
-  nextHop(span, seSelection, lastSelection);
-};
-
-const singleEdge = (edges: Cy.EdgeCollection): Cy.EdgeSingular | undefined => {
-  if (edges.length > 1) {
-    console.debug(`Expected singleton, found [${edges.length}] edges. Using first.`);
-  }
-  return edges.length > 0 ? edges[0] : undefined;
-};
-
-const singleNode = (nodes: Cy.NodeCollection): Cy.NodeSingular | undefined => {
-  if (nodes.length > 1) {
-    console.debug(`Expected singleton, found [${nodes.length}] nodes. Using first.`);
-  }
-  return nodes.length > 0 ? nodes[0] : undefined;
-};
-
-const addSpan = (ele: Cy.NodeSingular | Cy.EdgeSingular | undefined, span: Span): void => {
-  if (!ele) {
-    return;
-  }
-
-  if (ele.hasClass('span')) {
-    ele.data('spans').push(span);
-  } else {
-    ele.addClass('span');
-    ele.data('spans', [span]);
-  }
-};
-
-export const hideTrace = (cy: Cy.Core) => {
-  if (!cy) {
-    return;
-  }
-  // unhighlight old span-hits
-  const spanHits = cy.elements('*.span');
-  spanHits.removeClass('span');
-  spanHits.data('spans', undefined);
-};
-
-const getOutboundServiceEntry = (span: Span, cy: Cy.Core): Cy.NodeCollection | undefined => {
-  // see https://github.com/opentracing/specification/blob/master/semantic_conventions.md
-  if (span.tags.some(tag => tag.key === 'span.kind' && (tag.value === 'client' || tag.value === 'producer'))) {
-    return findServiceEntry(span, cy);
-  }
-  return undefined;
-};
-
-const getInboundServiceEntry = (span: Span, cy: Cy.Core): Cy.NodeCollection | undefined => {
-  // see https://github.com/opentracing/specification/blob/master/semantic_conventions.md
-  if (span.tags.some(tag => tag.key === 'span.kind' && (tag.value === 'server' || tag.value === 'consumer'))) {
-    return findServiceEntry(span, cy);
-  }
-  return undefined;
-};
-
-const findServiceEntry = (span: Span, cy: Cy.Core): Cy.NodeCollection | undefined => {
-  const hostname = span.tags.find(tag => tag.key === 'peer.hostname');
-  if (hostname && hostname.value !== '') {
-    return findSEHost(hostname.value, cy);
-  }
-  const addr = span.tags.find(tag => tag.key === 'peer.address');
-  if (addr && addr.value !== '') {
-    return findSEHost(addr.value.split(':')[0], cy);
-  }
-  return undefined;
-};
-
-const findSEHost = (hostname: string, cy: Cy.Core): Cy.NodeCollection | undefined => {
-  return cy.elements(`[${NodeAttr.nodeType}="${NodeType.SERVICE}"]`).filter(ele => {
-    const seInfo: SEInfo | undefined = ele.data(NodeAttr.isServiceEntry);
-    if (seInfo) {
-      // TODO: improve host matching, as "startsWith" allows false-positives
-      if (seInfo.hosts.some(h => h.startsWith(hostname))) {
-        return true;
-      }
-    }
-    return false;
-  });
-};
-
-const nextHop = (
-  span: Span,
-  next: Cy.NodeCollection | undefined,
-  last: Cy.NodeCollection | undefined
-): Cy.NodeCollection | undefined => {
-  if (!!next && next.length !== 0) {
-    const node = singleNode(next);
-    addSpan(node, span);
-    if (last) {
-      // Try both inbound and outbound, because of TCP edges where direction might not be correctly represented in graph
-      let edge = last.edgesTo(next);
-      if (!edge || edge.length === 0) {
-        edge = next.edgesTo(last);
-      }
-      addSpan(singleEdge(edge), span);
-    }
-    return next;
-  }
-  return last;
-};
diff --git a/frontend/src/components/CytoscapeGraph/FocusAnimation.ts b/frontend/src/components/CytoscapeGraph/FocusAnimation.ts
deleted file mode 100644
index d94168168..000000000
--- a/frontend/src/components/CytoscapeGraph/FocusAnimation.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { PFColorVals } from '../Pf/PfColors';
-
-// How fast refresh (frame rate)
-const FRAME_RATE = 1 / 30;
-// Radio of the biggest circle (i.e. when it starts)
-const MAX_RADIO = 60;
-const LINE_WIDTH = 1;
-
-// Time the animation will take in ms
-const ANIMATION_DURATION = 2000;
-
-type OnFinishedCallback = () => void;
-
-export class FocusAnimation {
-  private animationTimer;
-  private startTimestamp;
-  private elements;
-  private onFinishedCallback?: OnFinishedCallback;
-
-  private readonly layer;
-  private readonly context;
-
-  constructor(cy: any) {
-    this.layer = cy.cyCanvas();
-    this.context = this.layer.getCanvas().getContext('2d');
-
-    cy.one('destroy', () => this.stop());
-  }
-
-  onFinished(onFinishedCallback: OnFinishedCallback) {
-    this.onFinishedCallback = onFinishedCallback;
-  }
-
-  start(elements: any) {
-    this.stop();
-    this.elements = elements;
-    this.animationTimer = window.setInterval(this.processStep, FRAME_RATE * 1000);
-  }
-
-  stop() {
-    if (this.animationTimer) {
-      window.clearInterval(this.animationTimer);
-      this.animationTimer = undefined;
-      this.clear();
-    }
-  }
-
-  clear() {
-    this.layer.clear(this.context);
-  }
-
-  // This methods needs to be an arrow function.
-  // the reason is that we call this method from a window.setInterval and having an arrow function is a way to preserve
-  // "this".
-  processStep = () => {
-    try {
-      const current = Date.now();
-      if (this.startTimestamp === undefined) {
-        this.startTimestamp = current;
-      }
-      const step = (current - this.startTimestamp) / ANIMATION_DURATION;
-      this.layer.clear(this.context);
-      this.layer.setTransform(this.context);
-
-      if (step >= 1) {
-        this.stop();
-        if (this.onFinishedCallback) {
-          this.onFinishedCallback();
-        }
-        return;
-      }
-
-      this.elements.forEach(element => this.render(element, this.easingFunction(step) * MAX_RADIO));
-    } catch (exception) {
-      // If a step failed, the next step is likely to fail.
-      // Stop the rendering and throw the exception
-      this.stop();
-      throw exception;
-    }
-  };
-
-  private easingFunction(t: number) {
-    // Do a focus animation in, out and in again.
-    // Make the first focus slower and the subsequent bit faster
-    if (t < 0.5) {
-      // %50 of the time is spent on the first in
-      return 1 - t * 2;
-    } else if (t < 0.75) {
-      // 25% is spent in the out animation
-      return (t - 0.5) * 4;
-    }
-    return 1 - (t - 0.75) * 4; // 25% is spent in the second in
-  }
-
-  private getCenter(element: any) {
-    if (element.isNode()) {
-      return element.position();
-    } else {
-      return element.midpoint();
-    }
-  }
-
-  private render(element: any, radio: number) {
-    const { x, y } = this.getCenter(element);
-    this.context.strokeStyle = PFColorVals.Blue300;
-    this.context.lineWidth = LINE_WIDTH;
-    this.context.beginPath();
-    this.context.arc(x, y, radio, 0, 2 * Math.PI, true);
-    this.context.stroke();
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/Layout/BoxLayout.ts b/frontend/src/components/CytoscapeGraph/Layout/BoxLayout.ts
deleted file mode 100644
index 1b0b76a41..000000000
--- a/frontend/src/components/CytoscapeGraph/Layout/BoxLayout.ts
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
-  BoxLayout
-
-  This is a synthetic layout that helps to better layout the contents of box (i.e. compound)
-  nodes, in this way we ensure that the box nodes themselves are as small as possible, and avoiding
-  overlaps with other nodes.
-
-  It finishes by executing the default (i.e. user-selected) layout but prior to that will
-  individually layout the box node contents using the requested layouts.
-
-  Is composed of:
-   - The [optional] configured box layouts will layout the children of the box node.
-     - appBoxLayout, clusterBoxLayout, namespaceBoxLayout
-   - The [required] configured defaultLayout is used for any box layouts not otherwise specified, and is
-     applied for the final layout.
-   - A Synthetic edge generator creates synthetic edges (more info below).
-
-  The algorithm is roughly as follow:
-
-  1. For every box type (working inner to outer)
-       2. For every box node:
-          a. The box layout is run for every box and its relative positions (to the parent)
-             are saved for later use.
-          b. Get the resulting bounding box of the compound node, set the width and height of the node
-             using `cy.style`, so that the real layout honors the size when doing the layout.
-          c. For every edge that goes to a child (or comes from a child), create a synthetic edge
-            that goes to (or comes from) the compound node and remove the original
-            edge. We can cull away repeated edges as they are not needed.
-          d. Remove the children. This is important, else cytoscape won't honor the size specified
-             in previous step. "A compound parent node does not have independent dimensions (position
-             and size), as those values are automatically inferred by the positions and dimensions
-             of the descendant nodes." http://js.cytoscape.org/#notation/compound-nodes
-  3. Run the default layout on this new graph and wait until it finishes.
-     a. Restore the children.
-     b. Remove the synthetic edges.
-     c. For every child set the relative position to its parent
- */
-
-import { BoxByType, NodeAttr } from 'types/Graph';
-import { getLayoutByName } from '../graphs/LayoutDictionary';
-
-export const BOX_NODE_CLASS = '__boxNodeClass';
-
-const NAMESPACE_KEY = 'box-layout';
-const STYLES_KEY = NAMESPACE_KEY + 'styles';
-const RELATIVE_POSITION_KEY = NAMESPACE_KEY + 'relative_position';
-const PARENT_POSITION_KEY = NAMESPACE_KEY + '.parent_position';
-
-// Styles used to have more control on how the compound nodes are going to be seen by the Layout algorithm.
-type OverridenStyles = {
-  shape: string;
-  width: string;
-  height: string;
-};
-
-type LayoutBoxTypeResult = {
-  boxNodes: any;
-  syntheticEdges: any;
-  removedElements: any;
-};
-
-/**
- * Synthetic edge generator replaces edges to and from boxed nodes with edges to/from their boxes. Care is
- * taken to not generate duplicate edges when sourceA has multiple real edges into the same box.
- */
-class SyntheticEdgeGenerator {
-  private nextId = 0;
-  private generatedMap = {};
-
-  public getEdge(parentBoxType: BoxByType, source: any, target: any) {
-    const sourceId = this.normalizeToParent(parentBoxType, source).id();
-    const targetId = this.normalizeToParent(parentBoxType, target).id();
-
-    if (sourceId === targetId) {
-      return false;
-    }
-
-    const key = `${sourceId}->${targetId}`;
-
-    if (this.generatedMap[key]) {
-      return false;
-    }
-
-    this.generatedMap[key] = true;
-
-    return {
-      group: 'edges',
-      data: {
-        id: 'synthetic-edge-' + this.nextId++,
-        source: sourceId,
-        target: targetId
-      }
-    };
-  }
-
-  // Returns the element's parent if it exists and is also of the correct boxType.
-  private normalizeToParent(parentBoxType: BoxByType, element: any) {
-    const parent = element.parent();
-    return parent && parent.data(NodeAttr.isBox) === parentBoxType ? parent : element;
-  }
-}
-
-/**
- * Main class for the BoxLayout, used to bridge with cytoscape to make it easier to integrate with current code
- */
-export class BoxLayout {
-  readonly appBoxLayout;
-  readonly clusterBoxLayout;
-  readonly defaultLayout;
-  readonly namespaceBoxLayout;
-  readonly cy;
-  readonly elements;
-  readonly syntheticEdgeGenerator;
-
-  constructor(options: any) {
-    this.appBoxLayout = options.appBoxLayout || options.defaultLayout;
-    this.clusterBoxLayout = options.clusterBoxLayout || options.defaultLayout;
-    this.defaultLayout = options.defaultLayout;
-    this.namespaceBoxLayout = options.namespaceBoxLayout || options.defaultLayout;
-    this.cy = options.cy;
-    this.elements = options.eles;
-    this.syntheticEdgeGenerator = new SyntheticEdgeGenerator();
-  }
-
-  /**
-   * This code gets executed on the cy.layout(...).  run() is the entrypoint of this algorithm.
-   */
-  run() {
-    this.runAsync();
-  }
-
-  /**
-   * This is a stub required by cytoscape to allow the layout impl to emit events
-   * @param _events space separated string of event names
-   */
-  emit(_events) {
-    // intentionally empty
-  }
-
-  // Discrete layouts (dagre) always stop before layout.run() returns. Continuous layouts (cose,cola)
-  // are started by layout.run() but may stop after run() returns. Because outer boxes require the inner
-  // box layouts to complete, we need to force discrete behavior regardless of layout, and that is why
-  // this code is complicated with a variety of async handling.  Note that because namespace or cluster
-  // boxes may comprise large portions of the graph, we need to be flexible with the layout support (in
-  // other words, we can't force dagre like we do for app boxes).
-  async runAsync(): Promise<any> {
-    let allBoxNodes = this.cy.collection();
-    let removedElements = this.cy.collection();
-    let syntheticEdges = this.cy.collection();
-    let result;
-
-    // (1) working from inner boxing to outer boxing, perform the box layouts. the inner box layouts
-    // must complete before the outer box layouts can proceed.
-
-    result = await this.layoutBoxType(BoxByType.APP);
-    allBoxNodes = allBoxNodes.add(result.boxNodes);
-    removedElements = removedElements.add(result.removedElements);
-    syntheticEdges = syntheticEdges.add(result.syntheticEdges);
-
-    result = await this.layoutBoxType(BoxByType.NAMESPACE);
-    allBoxNodes = allBoxNodes.add(result.boxNodes);
-    removedElements = removedElements.add(result.removedElements);
-    syntheticEdges = syntheticEdges.add(result.syntheticEdges);
-
-    result = await this.layoutBoxType(BoxByType.CLUSTER);
-    allBoxNodes = allBoxNodes.add(result.boxNodes);
-    removedElements = removedElements.add(result.removedElements);
-    syntheticEdges = syntheticEdges.add(result.syntheticEdges);
-
-    // (3) perform the final layout...
-
-    // Before running the layout, reset the elements positions.
-    // This is not absolutely necessary, but without this we have seen some problems with
-    //  `cola` + firefox + a particular mesh
-    // Ensure we only touch the requested elements and not the whole graph.
-    const layoutElements = this.cy.collection().add(this.elements).subtract(removedElements).add(syntheticEdges);
-    layoutElements.position({ x: 0, y: 0 });
-
-    const layout = this.cy.layout({
-      // Create a new layout
-      ...getLayoutByName(this.defaultLayout), // Sharing the main options
-      eles: this.cy.elements() // and the current elements
-    });
-
-    // Add a one-time callback to be fired when the layout stops
-    layout.one('layoutstop', _event => {
-      // If we add any children back, our parent nodes position are going to take the bounding box's position of all
-      // their children. Before doing it, save this position in order to add this up to their children.
-      allBoxNodes.each(boxNode => {
-        boxNode.scratch(PARENT_POSITION_KEY, { ...boxNode.position() }); // Make a copy of the position, its an internal data from cy.
-      });
-
-      // (3.a) Add back the child nodes (with edges still attached)
-      removedElements.restore();
-
-      // (3.b) Remove synthetic edges
-      this.cy.remove(syntheticEdges);
-
-      // (3.c) Add and position the children nodes according to the layout
-      allBoxNodes.each(boxNode => {
-        const parentPosition = boxNode.scratch(PARENT_POSITION_KEY);
-        boxNode.children().each(child => {
-          const relativePosition = child.scratch(RELATIVE_POSITION_KEY);
-          child.position({
-            x: parentPosition.x + relativePosition.x,
-            y: parentPosition.y + relativePosition.y
-          });
-          child.removeData(RELATIVE_POSITION_KEY);
-        });
-
-        boxNode.style(boxNode.scratch(STYLES_KEY));
-        boxNode.removeClass(BOX_NODE_CLASS);
-
-        // Discard the saved values
-        boxNode.removeScratch(STYLES_KEY);
-        boxNode.removeScratch(PARENT_POSITION_KEY);
-      });
-
-      this.emit('layoutstop');
-    });
-
-    // Avoid propagating any local layout events up to cy, this would yield a global operation before the nodes are ready.
-    layout.on('layoutstart layoutready layoutstop', _event => {
-      return false;
-    });
-
-    layout.run();
-  }
-
-  async layoutBoxType(boxByType: BoxByType): Promise<LayoutBoxTypeResult> {
-    return new Promise((resolve, _reject) => {
-      const boxNodes = this.getBoxNodes(boxByType);
-
-      let boxLayoutOptions;
-      switch (boxByType) {
-        case BoxByType.APP:
-          boxLayoutOptions = getLayoutByName(this.appBoxLayout);
-          break;
-        case BoxByType.CLUSTER:
-          boxLayoutOptions = getLayoutByName(this.clusterBoxLayout);
-          break;
-        case BoxByType.NAMESPACE:
-          boxLayoutOptions = getLayoutByName(this.namespaceBoxLayout);
-          break;
-        default:
-          boxLayoutOptions = getLayoutByName(this.defaultLayout);
-      }
-
-      // Before completing work for the box type we must wait for all individual box work to complete
-      const boxNodePromises: Promise<any>[] = [];
-
-      // (2) Prepare each box node by assigning a size and running the compound layout
-      boxNodes.each(boxNode => {
-        const boxedNodes = boxNode.children();
-        const boxedElements = boxedNodes.add(boxedNodes.edgesTo(boxedNodes));
-        const boxLayout = boxedElements.layout(boxLayoutOptions);
-
-        boxNodePromises.push(
-          new Promise((resolve, _reject) => {
-            // (2.a) This promise resolves when the layout actually stops.
-            this.runLayout(boxLayoutOptions.name, boxLayout).then(_response => {
-              // (2.b) get the bounding box
-              // see https://github.com/cytoscape/cytoscape.js/issues/2402
-              const boundingBox = boxNode.boundingBox();
-
-              // Save the relative positions, as we will need them later.
-              boxedNodes.each(boxedNode => {
-                boxedNode.scratch(RELATIVE_POSITION_KEY, boxedNode.relativePosition());
-              });
-
-              const backupStyles: OverridenStyles = {
-                shape: boxNode.style('shape'),
-                height: boxNode.style('height'),
-                width: boxNode.style('width')
-              };
-
-              const newStyles: OverridenStyles = {
-                shape: 'rectangle',
-                height: `${boundingBox.h}px`,
-                width: `${boundingBox.w}px`
-              };
-
-              // Saves a backup of current styles to restore them after we finish
-              boxNode.scratch(STYLES_KEY, backupStyles);
-              boxNode.addClass(BOX_NODE_CLASS);
-
-              boxNode.style(newStyles);
-
-              resolve(true);
-            });
-          })
-        );
-      });
-
-      Promise.all(boxNodePromises).then(_results => {
-        let removedElements = this.cy.collection();
-        let syntheticEdges = this.cy.collection();
-
-        // (2.c) Add synthetic edges for every edge that touches a child node.
-        const boxedNodes = boxNodes.children();
-        for (const boxedNode of boxedNodes) {
-          for (const edge of boxedNode.connectedEdges()) {
-            const syntheticEdge = this.syntheticEdgeGenerator.getEdge(boxByType, edge.source(), edge.target());
-            if (syntheticEdge) {
-              syntheticEdges = syntheticEdges.add(this.cy.add(syntheticEdge));
-            }
-          }
-        }
-        // (2.d) Remove all child nodes from parents (and their edges).
-        removedElements = removedElements.add(this.cy.remove(boxedNodes));
-
-        resolve({ boxNodes: boxNodes, syntheticEdges: syntheticEdges, removedElements: removedElements });
-      });
-    });
-  }
-
-  async runLayout(_layoutName, layout): Promise<any> {
-    // Avoid propagating any local layout events up to cy, this would yield a global operation before the nodes are ready.
-    layout.on('layoutstart layoutready layoutstop', _event => {
-      return false;
-    });
-
-    // We know ALL algorithms (breadthfirst, dagre, grid, contentric) are discrete, we can resolve when run() returns
-    return layout.run();
-  }
-
-  getBoxNodes(boxByType: BoxByType): any {
-    return this.elements.nodes(`[${NodeAttr.isBox}="${boxByType}"]`);
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/Layout/KialiBreadthFirstLayout.ts b/frontend/src/components/CytoscapeGraph/Layout/KialiBreadthFirstLayout.ts
deleted file mode 100644
index c29137cd7..000000000
--- a/frontend/src/components/CytoscapeGraph/Layout/KialiBreadthFirstLayout.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
-  KialiBreadthFirstLayout
-
-  This is a version of the "breadthfirst" algorithm provided by Cytoscape at
-  https://github.com/cytoscape/cytoscape.js/blob/unstable/src/extensions/layout/breadthfirst.js
-
-  The standard algorithm needs some refinements for Kiali use cases and in this case it's simpler to clone it
-  rather than provide an extension.
-
- */
-
-const defaults = {
-  fit: true, // whether to fit the viewport to the graph
-  padding: 30, // padding on fit
-  spacingFactor: 1, // positive spacing factor, larger => more space between nodes (N.B. n/a if causes overlap)
-  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
-  avoidOverlap: true, // Not used, this algorithm always tries to avoid the overlap of the nodes
-  avoidOverlapPadding: 10, // extra spacing around nodes when avoidOverlap: true
-  nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
-  animate: false, // whether to transition the node positions
-  animationDuration: 500, // duration of animation in ms if enabled
-  animationEasing: undefined, // easing of animation if enabled,
-  animateFilter: function (_node, _i) {
-    return true;
-  }, // a function that determines whether the node should be animated.  All nodes animated by default on animate enabled.  Non-animated nodes are positioned immediately when the layout starts
-  ready: undefined, // callback on layoutready
-  stop: undefined, // callback on layoutstop
-  transform: function (_node, position) {
-    return position;
-  } // transform a given node position. Useful for changing flow direction in discrete layouts
-};
-
-// Helper functions to add breadthfirst level info into the cytospace model
-const getInfo = ele => ele.scratch('breadthfirst');
-const setInfo = (ele, obj) => ele.scratch('breadthfirst', obj);
-
-export class KialiBreadFirstLayout {
-  readonly cy;
-  readonly eles;
-  readonly options;
-
-  constructor(options: any) {
-    this.cy = options.cy;
-    this.eles = options.eles;
-    this.options = Object.assign({}, defaults, options);
-  }
-
-  /**
-   * This code gets executed on the cy.layout(...).  run() is the entrypoint of this algorithm.
-   */
-  run() {
-    var eles = this.eles;
-    var nodes = eles.nodes().not(':parent');
-    var graph = eles;
-    var options = this.options;
-
-    // Calculate roots from targets
-    // node.roots() doesn't work with compose nodes
-    // It calculates the "roots" from the edges
-    var targetIds = eles.edges().map(e => e.target().id());
-    var roots = nodes.filter(n => !targetIds.includes(n.id()));
-
-    var depths: any = [];
-    var foundByBfs = {};
-
-    var addToDepth = (ele, d) => {
-      if (depths[d] == null) {
-        depths[d] = [];
-      }
-
-      var i = depths[d].length;
-
-      depths[d].push(ele);
-
-      setInfo(ele, {
-        index: i,
-        depth: d
-      });
-    };
-
-    // Using Cytoscape algorithm to perform a breadthfirst search for the graph
-    // Annotate the nodes with the custom level
-    graph.bfs({
-      roots: roots,
-      directed: true,
-      visit: function (node, _edge, _pNode, _i, depth) {
-        let ele = node[0];
-        let id = ele.id();
-        addToDepth(ele, depth);
-        foundByBfs[id] = true;
-      }
-    });
-
-    // Check for nodes not found by bfs (i.e. no edges, iddle nodes)
-    var orphanNodes: any = [];
-    for (var i = 0; i < nodes.length; i++) {
-      var ele = nodes[i];
-
-      if (foundByBfs[ele.id()]) {
-        continue;
-      } else {
-        orphanNodes.push(ele);
-      }
-    }
-
-    // Assign the nodes a depth and index
-    var assignDepthsAt = function (i) {
-      var eles = depths[i];
-
-      for (var j = 0; j < eles.length; j++) {
-        var ele = eles[j];
-
-        if (ele == null) {
-          eles.splice(j, 1);
-          j--;
-          continue;
-        }
-
-        setInfo(ele, {
-          depth: i,
-          index: j
-        });
-      }
-    };
-
-    var assignDepths = function () {
-      for (var i = 0; i < depths.length; i++) {
-        assignDepthsAt(i);
-      }
-    };
-
-    assignDepths(); // clear holes
-
-    var sortNameFn = function (a, b) {
-      // Use id as default
-      var aName = a.id();
-      var bName = b.id();
-      var aNs = a.data('namespace') ? a.data('namespace') : '';
-      var bNs = b.data('namespace') ? b.data('namespace') : '';
-
-      if (a.data('workload')) {
-        aName = a.data('workload');
-      } else if (a.data('service')) {
-        aName = a.data('service');
-      } else if (a.data('app')) {
-        aName = a.data('app');
-      }
-
-      if (b.data('workload')) {
-        bName = b.data('workload');
-      } else if (b.data('service')) {
-        bName = b.data('service');
-      } else if (b.data('app')) {
-        bName = b.data('app');
-      }
-
-      // Including namespace in the id comparison
-      aName = aNs + '.' + aName;
-      bName = bNs + '.' + bName;
-
-      return aName.localeCompare(bName);
-    };
-
-    // Sort each level by name
-    // This loop can be optimized in the future to identify "closer" nodes
-    for (i = 0; i < depths.length; i++) {
-      // Sort elements by name in each row
-      depths[i].sort(sortNameFn);
-      assignDepthsAt(i);
-    }
-
-    // assign orphan nodes to a new top-level depth
-    var orphanDepth: any = [];
-    for (i = 0; i < orphanNodes.length; i++) {
-      orphanDepth.push(orphanNodes[i]);
-    }
-    if (depths.length > 0 && orphanDepth.length > 0) {
-      depths[0].unshift(...orphanDepth);
-    }
-
-    assignDepths();
-
-    // Precaltulated positions per level and index
-    var layoutPositions: any = [];
-
-    if (depths.length > 0) {
-      // Init the max width and height per level/row
-      var maxHWPerDepth: any = depths.map(_ => {
-        return {
-          maxw: 0,
-          maxh: 0
-        };
-      });
-      // Loop to calculate the max width and height per level/row
-      nodes.forEach(n => {
-        var { depth } = getInfo(n);
-        var nbb = n.layoutDimensions(options);
-        var w = nbb.w;
-        var h = nbb.h;
-        if (w > maxHWPerDepth[depth].maxw) {
-          maxHWPerDepth[depth].maxw = w;
-        }
-        if (h > maxHWPerDepth[depth].maxh) {
-          maxHWPerDepth[depth].maxh = h;
-        }
-      });
-
-      var posX = 0;
-      var posY = 0;
-      var splitOverflow = depths.length;
-      // Iterate per levels
-      for (i = 0; i < depths.length; i++) {
-        var level = depths[i];
-        var positions: any = [];
-
-        // Overflow:
-        // this is the key advantagge of this algorithm, to split a long row in multiple-rows for the same level.
-        // The logic is pretty basic, it overflows a line when it's len(row) > 2 x len(depth).
-        // This heuristic tries to present in mini-grids the line that tend to work better on large scenarios.
-        var isOverflow = false;
-        if (level.length >= depths.length * 2) {
-          isOverflow = true;
-          splitOverflow = Math.round(Math.sqrt(level.length));
-        }
-
-        // Iterate per row
-        // SplitJ is an index to manage the overflow of the lines
-        var lineMaxH = 0;
-        var lineOverflowH = 0;
-        for (var j = 0, splitJ = 0; j < level.length; j++, splitJ++) {
-          var lbb = level[j].layoutDimensions(options);
-          if (lbb.h > lineMaxH) {
-            lineMaxH = lbb.h;
-          }
-
-          if (isOverflow && splitJ > splitOverflow) {
-            // Reset the posX position
-            posX = 0;
-            // Increment the posY for the current splitted line
-            // 1/2 of current line
-            posY += Math.round(lineMaxH / 2);
-            // 1/2 of next line
-            var nextLineMaxH = 0;
-            for (var nextj = j; nextj < depths.length || nextj < level.length; nextj++) {
-              var nlbb = level[nextj].layoutDimensions(options);
-              if (nlbb.h > nextLineMaxH) {
-                nextLineMaxH = nlbb.h;
-              }
-            }
-            posY += Math.round(nextLineMaxH / 2);
-            splitJ = 0;
-            lineOverflowH = lineMaxH;
-            lineMaxH = 0;
-            nextLineMaxH = 0;
-          }
-          var pos = {
-            x: posX,
-            y: posY
-          };
-          posX += maxHWPerDepth[i].maxw + this.options.avoidOverlapPadding;
-          positions.push(pos);
-        }
-        posX = 0;
-
-        // Last line in overflow cases
-        if (isOverflow) {
-          posY += Math.round(lineOverflowH / 2);
-        } else {
-          posY += Math.round(maxHWPerDepth[i].maxh / 2);
-        }
-        if (i + 1 < depths.length) {
-          posY += Math.round(maxHWPerDepth[i + 1].maxh / 2);
-        }
-        // Position per row
-        layoutPositions.push(positions);
-        isOverflow = false;
-      }
-    }
-
-    var getPosition = function (ele) {
-      const eleInfo = getInfo(ele);
-      if (
-        eleInfo &&
-        eleInfo.hasOwnProperty('depth') &&
-        eleInfo.hasOwnProperty('index') &&
-        layoutPositions[eleInfo.depth]
-      ) {
-        return layoutPositions[eleInfo.depth][eleInfo.index];
-      }
-      return false;
-    };
-
-    eles.nodes().layoutPositions(this, options, getPosition);
-
-    return this;
-  }
-
-  /**
-   * This is a stub required by cytoscape to allow the layout impl to emit events
-   * @param _events space separated string of event names
-   */
-  emit(_events) {
-    // intentionally empty
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/Layout/KialiConcentricLayout.ts b/frontend/src/components/CytoscapeGraph/Layout/KialiConcentricLayout.ts
deleted file mode 100644
index 2d0659de8..000000000
--- a/frontend/src/components/CytoscapeGraph/Layout/KialiConcentricLayout.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
-  KialiConcentricLayout
-
-  This is a version of the "concentric" algorithm provided by Cytoscape at
-  https://github.com/cytoscape/cytoscape.js/blob/unstable/src/extensions/layout/concentric.js
-
-  The standard algorithm needs some refinements for Kiali use cases and in this case it's simpler to clone it
-  rather than provide an extension.
-
- */
-
-// Original defaults from concentric.js
-import { makeBoundingBox } from './KialiGridLayout';
-
-const defaults = {
-  fit: true, // whether to fit the viewport to the graph
-  padding: 30, // the padding on fit
-  startAngle: (3 / 2) * Math.PI, // where nodes start in radians
-  sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
-  clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
-  equidistant: false, // whether levels have an equal radial distance betwen them, may cause bounding box overflow
-  minNodeSpacing: 10, // min spacing between outside of nodes (used for radius adjustment)
-  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
-  avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
-  nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
-  height: undefined, // height of layout area (overrides container height)
-  width: undefined, // width of layout area (overrides container width)
-  spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
-  concentric: function (node) {
-    // returns numeric value for each node, placing higher nodes in levels towards the centre
-    return node.degree();
-  },
-  levelWidth: function (nodes) {
-    // the variation of concentric values in each level
-    return nodes.maxDegree() / 4;
-  },
-  animate: false, // whether to transition the node positions
-  animationDuration: 500, // duration of animation in ms if enabled
-  animationEasing: undefined, // easing of animation if enabled
-  animateFilter: function (_node, _i) {
-    return true;
-  }, // a function that determines whether the node should be animated.  All nodes animated by default on animate enabled.  Non-animated nodes are positioned immediately when the layout starts
-  ready: undefined, // callback on layoutready
-  stop: undefined, // callback on layoutstop
-  transform: function (_node, position) {
-    return position;
-  } // transform a given node position. Useful for changing flow direction in discrete layouts
-};
-
-export class KialiConcentricLayout {
-  readonly cy;
-  readonly eles;
-  readonly options;
-
-  constructor(options: any) {
-    this.cy = options.cy;
-    this.eles = options.eles;
-    this.options = Object.assign({}, defaults, options);
-  }
-
-  /**
-   * This code gets executed on the cy.layout(...).  run() is the entrypoint of this algorithm.
-   */
-  run() {
-    var clockwise =
-      this.options.counterclockwise !== undefined ? !this.options.counterclockwise : this.options.clockwise;
-
-    var cy = this.cy;
-    var eles = this.eles;
-    var nodes = eles.nodes().not(':parent');
-
-    var bb = makeBoundingBox(
-      this.options.boundingBox
-        ? this.options.boundingBox
-        : {
-            x1: 0,
-            y1: 0,
-            w: cy.width(),
-            h: cy.height()
-          }
-    );
-
-    var center = {
-      x: bb.x1 + bb.w / 2,
-      y: bb.y1 + bb.h / 2
-    };
-
-    var nodeValues: any = []; // { node, value }
-    var maxNodeSize = 0;
-
-    for (var i = 0; i < nodes.length; i++) {
-      var node = nodes[i];
-      var value;
-
-      // calculate the node value
-      value = this.options.concentric(node);
-      nodeValues.push({
-        value: value,
-        node: node
-      });
-
-      // for style mapping
-      node._private.scratch.concentric = value;
-    }
-
-    // in case we used the `concentric` in style
-    nodes.updateStyle();
-
-    // calculate max size now based on potentially updated mappers
-    for (i = 0; i < nodes.length; i++) {
-      node = nodes[i];
-      var nbb = node.layoutDimensions(this.options);
-
-      maxNodeSize = Math.max(maxNodeSize, nbb.w, nbb.h);
-    }
-
-    // sort node values in descreasing order
-    nodeValues.sort(function (a, b) {
-      return b.value - a.value;
-    });
-
-    var levelWidth = this.options.levelWidth(nodes);
-
-    // put the values into levels
-    var levels: any = [[]];
-    var currentLevel = levels[0];
-    for (i = 0; i < nodeValues.length; i++) {
-      var val = nodeValues[i];
-
-      if (currentLevel.length > 0) {
-        var diff = Math.abs(currentLevel[0].value - val.value);
-
-        if (diff >= levelWidth) {
-          currentLevel = [];
-          levels.push(currentLevel);
-        }
-      }
-
-      currentLevel.push(val);
-    }
-
-    // create positions from levels
-
-    var minDist = maxNodeSize + this.options.minNodeSpacing; // min dist between nodes
-
-    if (!this.options.avoidOverlap) {
-      // then strictly constrain to bb
-      var firstLvlHasMulti = levels.length > 0 && levels[0].length > 1;
-      var maxR = Math.min(bb.w, bb.h) / 2 - minDist;
-      var rStep = maxR / (levels.length + firstLvlHasMulti ? 1 : 0);
-
-      minDist = Math.min(minDist, rStep);
-    }
-
-    // find the metrics for each level
-    var r = 0;
-    for (let i = 0; i < levels.length; i++) {
-      var level = levels[i];
-      var sweep = this.options.sweep === undefined ? 2 * Math.PI - (2 * Math.PI) / level.length : this.options.sweep;
-      var dTheta = (level.dTheta = sweep / Math.max(1, level.length - 1));
-
-      // calculate the radius
-      if (level.length > 1 && this.options.avoidOverlap) {
-        // but only if more than one node (can't overlap)
-        var dcos = Math.cos(dTheta) - Math.cos(0);
-        var dsin = Math.sin(dTheta) - Math.sin(0);
-        var rMin = Math.sqrt((minDist * minDist) / (dcos * dcos + dsin * dsin)); // s.t. no nodes overlapping
-
-        r = Math.max(rMin, r);
-      }
-
-      level.r = r;
-
-      r += minDist;
-    }
-
-    if (this.options.equidistant) {
-      var rDeltaMax = 0;
-      r = 0;
-
-      for (i = 0; i < levels.length; i++) {
-        level = levels[i];
-        var rDelta = level.r - r;
-
-        rDeltaMax = Math.max(rDeltaMax, rDelta);
-      }
-
-      r = 0;
-      for (i = 0; i < levels.length; i++) {
-        level = levels[i];
-
-        if (i === 0) {
-          r = level.r;
-        }
-
-        level.r = r;
-
-        r += rDeltaMax;
-      }
-    }
-
-    // calculate the node positions
-    var pos = {}; // id => position
-    for (i = 0; i < levels.length; i++) {
-      level = levels[i];
-      dTheta = level.dTheta;
-      r = level.r;
-
-      for (var j = 0; j < level.length; j++) {
-        val = level[j];
-        var theta = this.options.startAngle + (clockwise ? 1 : -1) * dTheta * j;
-
-        var p = {
-          x: center.x + r * Math.cos(theta),
-          y: center.y + r * Math.sin(theta)
-        };
-
-        pos[val.node.id()] = p;
-      }
-    }
-
-    // position the nodes
-    eles.nodes().layoutPositions(this, this.options, function (ele) {
-      let id = ele.id();
-
-      return pos[id];
-    });
-
-    return this;
-  }
-
-  /**
-   * This is a stub required by cytoscape to allow the layout impl to emit events
-   * @param _events space separated string of event names
-   */
-  emit(_events) {
-    // intentionally empty
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/Layout/KialiDagreLayout.ts b/frontend/src/components/CytoscapeGraph/Layout/KialiDagreLayout.ts
deleted file mode 100644
index b698718ba..000000000
--- a/frontend/src/components/CytoscapeGraph/Layout/KialiDagreLayout.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
-  KialiDagreLayout
-
-  This is a version of the "dagre" algorithm provided by Cytoscape at
-  https://github.com/cytoscape/cytoscape.js-dagre/blob/master/src/layout.js
-
-  The standard algorithm needs some refinements for Kiali use cases and in this case it's simpler to clone it
-  rather than provide an extension.
-
- */
-
-import dagre from 'dagre';
-
-const defaults = {
-  // dagre algo options, uses default value on undefined
-  nodeSep: undefined, // the separation between adjacent nodes in the same rank
-  edgeSep: undefined, // the separation between adjacent edges in the same rank
-  rankSep: undefined, // the separation between adjacent nodes in the same rank
-  rankDir: undefined, // 'TB' for top to bottom flow, 'LR' for left to right,
-  align: undefined, // alignment for rank nodes. Can be 'UL', 'UR', 'DL', or 'DR', where U = up, D = down, L = left, and R = right
-  acyclicer: undefined, // If set to 'greedy', uses a greedy heuristic for finding a feedback arc set for a graph.
-  // A feedback arc set is a set of edges that can be removed to make a graph acyclic.
-  ranker: undefined, // Type of algorithm to assigns a rank to each node in the input graph.
-  // Possible values: network-simplex, tight-tree or longest-path
-  minLen: function (_edge) {
-    return 1;
-  }, // number of ranks to keep between the source and target of the edge
-  edgeWeight: function (_edge) {
-    return 1;
-  }, // higher weight edges are generally made shorter and straighter than lower weight edges
-
-  // general layout options
-  fit: true, // whether to fit to viewport
-  padding: 30, // fit padding
-  spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
-  nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node
-  animate: false, // whether to transition the node positions
-  animateFilter: function (_node, _i) {
-    return true;
-  }, // whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions
-  animationDuration: 500, // duration of animation in ms if enabled
-  animationEasing: undefined, // easing of animation if enabled
-  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
-  transform: function (_node, pos) {
-    return pos;
-  }, // a function that applies a transform to the final node position
-  ready: function () {}, // on layoutready
-  stop: function () {} // on layoutstop
-};
-
-const isFunction = function (o) {
-  return typeof o === 'function';
-};
-
-export class KialiDagreLayout {
-  readonly cy;
-  readonly eles;
-  readonly options;
-
-  constructor(options: any) {
-    this.cy = options.cy;
-    this.eles = options.eles;
-    this.options = Object.assign({}, defaults, options);
-  }
-
-  /**
-   * This code gets executed on the cy.layout(...).  run() is the entrypoint of this algorithm.
-   */
-  run() {
-    var cy = this.cy;
-    var eles = this.eles;
-    var options = this.options;
-
-    var getVal = function (ele, val) {
-      return isFunction(val) ? val.apply(ele, [ele]) : val;
-    };
-
-    var bb = options.boundingBox || { x1: 0, y1: 0, w: cy.width(), h: cy.height() };
-    if (bb.x2 === undefined) {
-      bb.x2 = bb.x1 + bb.w;
-    }
-    if (bb.w === undefined) {
-      bb.w = bb.x2 - bb.x1;
-    }
-    if (bb.y2 === undefined) {
-      bb.y2 = bb.y1 + bb.h;
-    }
-    if (bb.h === undefined) {
-      bb.h = bb.y2 - bb.y1;
-    }
-
-    var g = new dagre.graphlib.Graph({
-      multigraph: true,
-      compound: true
-    });
-
-    var gObj = {};
-    var setGObj = function (name, val) {
-      if (val != null) {
-        gObj[name] = val;
-      }
-    };
-
-    setGObj('nodesep', options.nodeSep);
-    setGObj('edgesep', options.edgeSep);
-    setGObj('ranksep', options.rankSep);
-    setGObj('rankdir', options.rankDir);
-    setGObj('align', options.align);
-    setGObj('ranker', options.ranker);
-    setGObj('acyclicer', options.acyclicer);
-
-    g.setGraph(gObj);
-
-    g.setDefaultEdgeLabel(function () {
-      return {};
-    });
-    g.setDefaultNodeLabel(function () {
-      return {};
-    });
-
-    // add nodes to dagre
-    var nodes = eles.nodes();
-    for (var i = 0; i < nodes.length; i++) {
-      var node = nodes[i];
-      var nbb = node.layoutDimensions(options);
-
-      g.setNode(node.id(), {
-        width: nbb.w,
-        height: nbb.h,
-        name: node.id()
-      });
-    }
-
-    // set compound parents
-    for (i = 0; i < nodes.length; i++) {
-      node = nodes[i];
-
-      if (node.isChild()) {
-        g.setParent(node.id(), node.parent().id());
-      }
-    }
-
-    // add edges to dagre
-    var edges = eles.edges().stdFilter(function (edge) {
-      return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes
-    });
-
-    for (i = 0; i < edges.length; i++) {
-      var edge = edges[i];
-
-      g.setEdge(
-        edge.source().id(),
-        edge.target().id(),
-        {
-          minlen: getVal(edge, options.minLen),
-          weight: getVal(edge, options.edgeWeight),
-          name: edge.id()
-        },
-        edge.id()
-      );
-    }
-
-    dagre.layout(g);
-
-    var gNodeIds = g.nodes();
-    for (i = 0; i < gNodeIds.length; i++) {
-      var id = gNodeIds[i];
-      var n = g.node(id);
-
-      cy.getElementById(id).scratch().dagre = n;
-    }
-
-    var dagreBB;
-
-    if (options.boundingBox) {
-      dagreBB = { x1: Infinity, x2: -Infinity, y1: Infinity, y2: -Infinity };
-      nodes.forEach(function (node) {
-        let dModel = node.scratch().dagre;
-
-        dagreBB.x1 = Math.min(dagreBB.x1, dModel.x);
-        dagreBB.x2 = Math.max(dagreBB.x2, dModel.x);
-
-        dagreBB.y1 = Math.min(dagreBB.y1, dModel.y);
-        dagreBB.y2 = Math.max(dagreBB.y2, dModel.y);
-      });
-
-      dagreBB.w = dagreBB.x2 - dagreBB.x1;
-      dagreBB.h = dagreBB.y2 - dagreBB.y1;
-    } else {
-      dagreBB = bb;
-    }
-
-    var constrainPos = function (p) {
-      if (options.boundingBox) {
-        let xPct = dagreBB.w === 0 ? 0 : (p.x - dagreBB.x1) / dagreBB.w;
-        let yPct = dagreBB.h === 0 ? 0 : (p.y - dagreBB.y1) / dagreBB.h;
-
-        return {
-          x: bb.x1 + xPct * bb.w,
-          y: bb.y1 + yPct * bb.h
-        };
-      } else {
-        return p;
-      }
-    };
-
-    nodes.layoutPositions(this, options, function (ele) {
-      let dModel = ele.scratch().dagre;
-
-      return constrainPos({
-        x: dModel.x,
-        y: dModel.y
-      });
-    });
-
-    return this;
-  }
-
-  /**
-   * This is a stub required by cytoscape to allow the layout impl to emit events
-   * @param _events space separated string of event names
-   */
-  emit(_events) {
-    // intentionally empty
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/Layout/KialiGridLayout.ts b/frontend/src/components/CytoscapeGraph/Layout/KialiGridLayout.ts
deleted file mode 100644
index 8365ba925..000000000
--- a/frontend/src/components/CytoscapeGraph/Layout/KialiGridLayout.ts
+++ /dev/null
@@ -1,321 +0,0 @@
-/*
-  KialiGridLayout
-
-  This is a version of the "grid" algorithm provided by Cytoscape at
-  https://github.com/cytoscape/cytoscape.js/blob/unstable/src/extensions/layout/grid.js
-
-  The standard algorithm needs some refinements for Kiali use cases and in this case it's simpler to clone it
-  rather than provide an extension.
-
- */
-
-// Original defaults from grid.js
-const defaults = {
-  fit: true, // whether to fit the viewport to the graph
-  padding: 30, // padding used on fit
-  boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
-  avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
-  avoidOverlapPadding: 10, // extra spacing around nodes when avoidOverlap: true
-  nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
-  spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
-  condense: false, // uses all available space on false, uses minimal space on true
-  rows: undefined, // force num of rows in the grid
-  cols: undefined, // force num of columns in the grid
-  position: function (_node) {}, // returns { row, col } for element
-  sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
-  animate: false, // whether to transition the node positions
-  animationDuration: 500, // duration of animation in ms if enabled
-  animationEasing: undefined, // easing of animation if enabled
-  animateFilter: function (_node, _i) {
-    return true;
-  }, // a function that determines whether the node should be animated.  All nodes animated by default on animate enabled.  Non-animated nodes are positioned immediately when the layout starts
-  ready: undefined, // callback on layoutready
-  stop: undefined, // callback on layoutstop
-  transform: function (_node, position) {
-    return position;
-  } // transform a given node position. Useful for changing flow direction in discrete layouts
-};
-
-// makes a full bb (x1, y1, x2, y2, w, h) from implicit params
-export const makeBoundingBox = bb => {
-  const defaultBB = {
-    x1: Infinity,
-    y1: Infinity,
-    x2: -Infinity,
-    y2: -Infinity,
-    w: 0,
-    h: 0
-  };
-  if (bb == null) {
-    return defaultBB;
-  }
-  if (bb.x1 != null && bb.y1 != null) {
-    if (bb.x2 != null && bb.y2 != null && bb.x2 >= bb.x1 && bb.y2 >= bb.y1) {
-      return {
-        x1: bb.x1,
-        y1: bb.y1,
-        x2: bb.x2,
-        y2: bb.y2,
-        w: bb.x2 - bb.x1,
-        h: bb.y2 - bb.y1
-      };
-    } else if (bb.w != null && bb.h != null && bb.w >= 0 && bb.h >= 0) {
-      return {
-        x1: bb.x1,
-        y1: bb.y1,
-        x2: bb.x1 + bb.w,
-        y2: bb.y1 + bb.h,
-        w: bb.w,
-        h: bb.h
-      };
-    }
-  }
-  return defaultBB;
-};
-
-export class KialiGridLayout {
-  readonly cy;
-  readonly eles;
-  readonly options;
-
-  constructor(options: any) {
-    this.cy = options.cy;
-    this.eles = options.eles;
-    this.options = Object.assign({}, defaults, options);
-  }
-
-  /**
-   * This code gets executed on the cy.layout(...).  run() is the entrypoint of this algorithm.
-   */
-  run() {
-    var cy = this.cy;
-    var eles = this.eles;
-    var nodes = eles.nodes().not(':parent');
-
-    if (this.options.sort) {
-      nodes = nodes.sort(this.options.sort);
-    }
-
-    var bb = makeBoundingBox(
-      this.options.boundingBox
-        ? this.options.boundingBox
-        : {
-            x1: 0,
-            y1: 0,
-            w: cy.width(),
-            h: cy.height()
-          }
-    );
-
-    // Edge case
-    if (bb.h === 0 || bb.w === 0) {
-      eles.nodes().layoutPositions(this, this.options, function (_ele) {
-        return { x: bb.x1, y: bb.y1 };
-      });
-      return this;
-    }
-
-    // width/height * splits^2 = cells where splits is number of times to split width
-    var cells = nodes.size();
-    var splits = Math.sqrt((cells * bb.h) / bb.w);
-    var rows = Math.round(splits);
-    var cols = Math.round((bb.w / bb.h) * splits);
-
-    var small = function (val) {
-      if (val == null) {
-        return Math.min(rows, cols);
-      } else {
-        var min = Math.min(rows, cols);
-        if (min === rows) {
-          rows = val;
-        } else {
-          cols = val;
-        }
-        return min;
-      }
-    };
-
-    var large = function (val) {
-      if (val == null) {
-        return Math.max(rows, cols);
-      } else {
-        var max = Math.max(rows, cols);
-        if (max === rows) {
-          rows = val;
-        } else {
-          cols = val;
-        }
-        return max;
-      }
-    };
-
-    var oRows = this.options.rows;
-    var oCols = this.options.cols != null ? this.options.cols : this.options.columns;
-
-    // if rows or columns were set in options, use those values
-    if (oRows != null && oCols != null) {
-      rows = oRows;
-      cols = oCols;
-    } else if (oRows != null && oCols == null) {
-      rows = oRows;
-      cols = Math.ceil(cells / rows);
-    } else if (oRows == null && oCols != null) {
-      cols = oCols;
-      rows = Math.ceil(cells / cols);
-    }
-
-    // otherwise use the automatic values and adjust accordingly
-
-    // if rounding was up, see if we can reduce rows or columns
-    else if (cols * rows > cells) {
-      var sm = small(null);
-      var lg = large(null);
-
-      // reducing the small side takes away the most cells, so try it first
-      if ((sm - 1) * lg >= cells) {
-        small(sm - 1);
-      } else if ((lg - 1) * sm >= cells) {
-        large(lg - 1);
-      }
-    } else {
-      // if rounding was too low, add rows or columns
-      while (cols * rows < cells) {
-        sm = small(null);
-        lg = large(null);
-
-        // try to add to larger side first (adds less in multiplication)
-        if ((lg + 1) * sm >= cells) {
-          large(lg + 1);
-        } else {
-          small(sm + 1);
-        }
-      }
-    }
-
-    var cellWidth = bb.w / cols;
-    var cellHeight = bb.h / rows;
-
-    if (this.options.condense) {
-      cellWidth = 0;
-      cellHeight = 0;
-    }
-
-    if (this.options.avoidOverlap) {
-      for (var i = 0; i < nodes.length; i++) {
-        var node = nodes[i];
-        var pos = node._private.position;
-
-        if (pos.x == null || pos.y == null) {
-          // for bb
-          pos.x = 0;
-          pos.y = 0;
-        }
-
-        var nbb = node.layoutDimensions(this.options);
-        var p = this.options.avoidOverlapPadding;
-
-        var w = nbb.w + p;
-        var h = nbb.h + p;
-
-        cellWidth = Math.max(cellWidth, w);
-        cellHeight = Math.max(cellHeight, h);
-      }
-    }
-
-    var cellUsed = {}; // e.g. 'c-0-2' => true
-
-    var used = function (row, col) {
-      return cellUsed['c-' + row + '-' + col] ? true : false;
-    };
-
-    var use = function (row, col) {
-      cellUsed['c-' + row + '-' + col] = true;
-    };
-
-    // to keep track of current cell position
-    var row = 0;
-    var col = 0;
-    var moveToNextCell = function () {
-      col++;
-      if (col >= cols) {
-        col = 0;
-        row++;
-      }
-    };
-
-    // get a cache of all the manual positions
-    var id2manPos = {};
-    for (i = 0; i < nodes.length; i++) {
-      node = nodes[i];
-      var rcPos = this.options.position(node);
-
-      if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) {
-        // must have at least row or col def'd
-        pos = {
-          row: rcPos.row,
-          col: rcPos.col
-        };
-
-        if (pos.col === undefined) {
-          // find unused col
-          pos.col = 0;
-
-          while (used(pos.row, pos.col)) {
-            pos.col++;
-          }
-        } else if (pos.row === undefined) {
-          // find unused row
-          pos.row = 0;
-
-          while (used(pos.row, pos.col)) {
-            pos.row++;
-          }
-        }
-
-        id2manPos[node.id()] = pos;
-        use(pos.row, pos.col);
-      }
-    }
-
-    var getPos = function (element, _i) {
-      var x, y;
-
-      if (element.locked() || element.isParent()) {
-        return false;
-      }
-
-      // see if we have a manual position set
-      var rcPos = id2manPos[element.id()];
-      if (rcPos) {
-        x = rcPos.col * cellWidth + cellWidth / 2 + bb.x1;
-        y = rcPos.row * cellHeight + cellHeight / 2 + bb.y1;
-      } else {
-        // otherwise set automatically
-
-        while (used(row, col)) {
-          moveToNextCell();
-        }
-
-        x = col * cellWidth + cellWidth / 2 + bb.x1;
-        y = row * cellHeight + cellHeight / 2 + bb.y1;
-        use(row, col);
-
-        moveToNextCell();
-      }
-
-      return { x: x, y: y };
-    };
-
-    nodes.layoutPositions(this, this.options, getPos);
-
-    return this;
-  }
-
-  /**
-   * This is a stub required by cytoscape to allow the layout impl to emit events
-   * @param _events space separated string of event names
-   */
-  emit(_events) {
-    // intentionally empty
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/MiniGraphCard.tsx b/frontend/src/components/CytoscapeGraph/MiniGraphCard.tsx
deleted file mode 100644
index f35782d17..000000000
--- a/frontend/src/components/CytoscapeGraph/MiniGraphCard.tsx
+++ /dev/null
@@ -1,390 +0,0 @@
-import * as React from 'react';
-import { connect } from 'react-redux';
-import {
-  Card,
-  CardBody,
-  CardHeader,
-  CardTitle,
-  Dropdown,
-  DropdownItem,
-  DropdownList,
-  MenuToggle,
-  MenuToggleElement,
-  ToolbarItem
-} from '@patternfly/react-core';
-import { router } from '../../app/History';
-import { GraphDataSource } from '../../services/GraphDataSource';
-import { DecoratedGraphElements, EdgeMode, GraphType, NodeType } from '../../types/Graph';
-import { CytoscapeGraph, GraphEdgeTapEvent, GraphNodeTapEvent } from './CytoscapeGraph';
-import { GraphUrlParams, makeNodeGraphUrlFromParams } from 'components/Nav/NavUtils';
-import { store } from 'store/ConfigStore';
-import { kialiStyle } from 'styles/StyleUtils';
-import { toRangeString } from '../Time/Utils';
-import { TimeInMilliseconds } from '../../types/Common';
-import { ServiceDetailsInfo } from '../../types/ServiceInfo';
-import { KialiDagreGraph } from './graphs/KialiDagreGraph';
-import { KialiAppState } from '../../store/Store';
-import { isKiosk, isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions';
-import { ServiceWizardActionsDropdownGroup } from '../IstioWizards/ServiceWizardActionsDropdownGroup';
-import { WizardAction, WizardMode } from '../IstioWizards/WizardActions';
-import { TimeDurationModal } from '../Time/TimeDurationModal';
-import { TimeDurationIndicator } from '../Time/TimeDurationIndicator';
-import { KioskElement } from '../Kiosk/KioskElement';
-import { GraphSelectorBuilder } from 'pages/Graph/GraphSelector';
-import { isMultiCluster } from '../../config';
-import { KialiIcon } from 'config/KialiIcon';
-import { kebabToggleStyle } from 'styles/DropdownStyles';
-import { Workload } from 'types/Workload';
-import { WorkloadWizardActionsDropdownGroup } from 'components/IstioWizards/WorkloadWizardActionsDropdownGroup';
-
-const initGraphContainerStyle = kialiStyle({ width: '100%', height: '100%' });
-
-type ReduxProps = {
-  kiosk: string;
-  theme: string;
-};
-
-type MiniGraphCardProps = ReduxProps & {
-  dataSource: GraphDataSource;
-  graphContainerStyle?: string;
-  namespace?: string;
-  onDeleteTrafficRouting?: (key: string) => void;
-  onEdgeTap?: (e: GraphEdgeTapEvent) => void;
-  onLaunchWizard?: (key: WizardAction, mode: WizardMode) => void;
-  refreshWorkload?: () => void;
-  serviceDetails?: ServiceDetailsInfo | null;
-  workload?: Workload | null;
-};
-
-type MiniGraphCardState = {
-  graphData: DecoratedGraphElements;
-  isKebabOpen: boolean;
-  isTimeOptionsOpen: boolean;
-};
-
-class MiniGraphCardComponent extends React.Component<MiniGraphCardProps, MiniGraphCardState> {
-  private cytoscapeGraphRef: any;
-
-  constructor(props: MiniGraphCardProps) {
-    super(props);
-
-    this.cytoscapeGraphRef = React.createRef();
-    this.state = { isKebabOpen: false, isTimeOptionsOpen: false, graphData: props.dataSource.graphData };
-  }
-
-  componentDidMount(): void {
-    this.props.dataSource.on('fetchSuccess', this.refresh);
-    this.props.dataSource.on('fetchError', this.refresh);
-  }
-
-  componentWillUnmount(): void {
-    this.props.dataSource.removeListener('fetchSuccess', this.refresh);
-    this.props.dataSource.removeListener('fetchError', this.refresh);
-  }
-
-  private refresh = (): void => {
-    this.setState({ graphData: this.props.dataSource.graphData });
-  };
-
-  render(): React.ReactNode {
-    const graphCardActions = [
-      <DropdownItem key="viewFullGraph" onClick={this.onViewFullGraph}>
-        Show full graph
-      </DropdownItem>,
-      <DropdownItem key="viewNodeGraph" onClick={this.onViewNodeGraph}>
-        Show node graph
-      </DropdownItem>
-    ];
-
-    if (isKiosk(this.props.kiosk)) {
-      if (this.props.workload && this.props.namespace) {
-        graphCardActions.push(
-          <WorkloadWizardActionsDropdownGroup
-            actionsLabel={true}
-            namespace={this.props.namespace}
-            onAction={this.handleWorkloadAction}
-            workload={this.props.workload}
-          ></WorkloadWizardActionsDropdownGroup>
-        );
-      } else if (this.props.serviceDetails) {
-        graphCardActions.push(
-          <ServiceWizardActionsDropdownGroup
-            virtualServices={this.props.serviceDetails.virtualServices ?? []}
-            destinationRules={this.props.serviceDetails.destinationRules ?? []}
-            k8sGRPCRoutes={this.props.serviceDetails.k8sGRPCRoutes ?? []}
-            k8sHTTPRoutes={this.props.serviceDetails.k8sHTTPRoutes ?? []}
-            istioPermissions={this.props.serviceDetails.istioPermissions}
-            onAction={this.handleLaunchWizard}
-            onDelete={this.handleDeleteTrafficRouting}
-          />
-        );
-      }
-    }
-
-    const rangeEnd: TimeInMilliseconds = this.props.dataSource.graphTimestamp * 1000;
-    const rangeStart: TimeInMilliseconds = rangeEnd - this.props.dataSource.graphDuration * 1000;
-    const intervalTitle =
-      rangeEnd > 0 ? toRangeString(rangeStart, rangeEnd, { second: '2-digit' }, { second: '2-digit' }) : 'Loading';
-
-    return (
-      <>
-        <Card style={{ height: '100%' }} id={'MiniGraphCard'} data-test="mini-graph">
-          <CardHeader
-            actions={{
-              actions: (
-                <>
-                  <KioskElement>
-                    <ToolbarItem>
-                      <TimeDurationIndicator onClick={this.toggleTimeOptionsVisibility} isDuration={true} />
-                    </ToolbarItem>
-                  </KioskElement>
-
-                  <Dropdown
-                    toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
-                      <MenuToggle
-                        id="minigraph-toggle"
-                        ref={toggleRef}
-                        className={kebabToggleStyle}
-                        aria-label="Actions"
-                        variant="plain"
-                        onClick={() => this.onGraphActionsToggle(!this.state.isKebabOpen)}
-                        isExpanded={this.state.isKebabOpen}
-                      >
-                        <KialiIcon.KebabToggle />
-                      </MenuToggle>
-                    )}
-                    isOpen={this.state.isKebabOpen}
-                    onOpenChange={(isOpen: boolean) => this.onGraphActionsToggle(isOpen)}
-                    popperProps={{ position: 'right' }}
-                  >
-                    <DropdownList>{graphCardActions}</DropdownList>
-                  </Dropdown>
-                </>
-              ),
-              hasNoOffset: false,
-              className: undefined
-            }}
-          >
-            <CardTitle style={{ float: 'left' }}>{intervalTitle}</CardTitle>
-          </CardHeader>
-
-          <CardBody>
-            <div style={{ height: '100%' }}>
-              <CytoscapeGraph
-                containerClassName={
-                  this.props.graphContainerStyle ? this.props.graphContainerStyle : initGraphContainerStyle
-                }
-                graphData={{
-                  elements: this.state.graphData,
-                  elementsChanged: true,
-                  errorMessage: !!this.props.dataSource.errorMessage ? this.props.dataSource.errorMessage : undefined,
-                  isError: this.props.dataSource.isError,
-                  isLoading: this.props.dataSource.isLoading,
-                  fetchParams: this.props.dataSource.fetchParameters,
-                  timestamp: this.props.dataSource.graphTimestamp
-                }}
-                toggleIdleNodes={() => undefined}
-                edgeLabels={this.props.dataSource.fetchParameters.edgeLabels}
-                edgeMode={EdgeMode.ALL}
-                isMiniGraph={true}
-                onEdgeTap={this.props.onEdgeTap}
-                layout={KialiDagreGraph.getLayout()}
-                namespaceLayout={KialiDagreGraph.getLayout()}
-                onNodeTap={this.handleNodeTap}
-                // Ranking not enabled for minigraphs yet
-                rankBy={[]}
-                ref={refInstance => this.setCytoscapeGraph(refInstance)}
-                refreshInterval={0}
-                setRankResult={undefined}
-                showIdleEdges={false}
-                showOperationNodes={false}
-                showOutOfMesh={true}
-                showRank={false}
-                showSecurity={true}
-                showServiceNodes={true}
-                showTrafficAnimation={false}
-                showIdleNodes={false}
-                showVirtualServices={true}
-                summaryData={null}
-                theme={this.props.theme}
-              />
-            </div>
-          </CardBody>
-        </Card>
-
-        <TimeDurationModal
-          customDuration={false}
-          isOpen={this.state.isTimeOptionsOpen}
-          onConfirm={this.toggleTimeOptionsVisibility}
-          onCancel={this.toggleTimeOptionsVisibility}
-        />
-      </>
-    );
-  }
-
-  private setCytoscapeGraph = (cytoscapeGraph: any): void => {
-    this.cytoscapeGraphRef.current = cytoscapeGraph;
-  };
-
-  private handleLaunchWizard = (key: WizardAction, mode: WizardMode): void => {
-    this.onGraphActionsToggle(false);
-    if (this.props.onLaunchWizard) {
-      this.props.onLaunchWizard(key, mode);
-    }
-  };
-
-  private handleDeleteTrafficRouting = (key: string): void => {
-    this.onGraphActionsToggle(false);
-    if (this.props.onDeleteTrafficRouting) {
-      this.props.onDeleteTrafficRouting(key);
-    }
-  };
-
-  private handleWorkloadAction = (_key: string): void => {
-    this.onGraphActionsToggle(false);
-    if (this.props.refreshWorkload) {
-      this.props.refreshWorkload();
-    }
-  };
-
-  private handleNodeTap = (e: GraphNodeTapEvent): void => {
-    // Do nothing on inaccessible nodes or service entry nodes
-    if (e.isInaccessible || e.isServiceEntry) {
-      return;
-    }
-
-    // If we are already on the details page of the tapped node, do nothing.
-    const displayedNode = this.props.dataSource.fetchParameters.node;
-
-    // Minigraph will consider box nodes as app
-    const eNodeType = e.nodeType === 'box' && e.isBox ? e.isBox : e.workload ? 'workload' : e.nodeType;
-    const isSameResource =
-      displayedNode?.namespace.name === e.namespace &&
-      displayedNode.nodeType === eNodeType &&
-      displayedNode[displayedNode.nodeType] === e[eNodeType];
-
-    if (isSameResource) {
-      return;
-    }
-
-    // unselect the currently selected node
-    const cy = this.cytoscapeGraphRef.current.getCy();
-    if (cy) {
-      cy.$(':selected').selectify().unselect().unselectify();
-    }
-
-    // Redirect to the details page of the tapped node.
-    let resource = e[eNodeType];
-    let resourceType: string = eNodeType === NodeType.APP ? 'application' : eNodeType;
-
-    let href = `/namespaces/${e.namespace}/${resourceType}s/${resource}`;
-
-    if (e.cluster && isMultiCluster) {
-      href = `${href}?clusterName=${e.cluster}`;
-    }
-
-    if (isParentKiosk(this.props.kiosk)) {
-      kioskContextMenuAction(href);
-    } else {
-      router.navigate(href);
-    }
-  };
-
-  private onGraphActionsToggle = (isOpen: boolean): void => {
-    this.setState({
-      isKebabOpen: isOpen
-    });
-  };
-
-  private onViewFullGraph = (): void => {
-    const namespace = this.props.dataSource.fetchParameters.namespaces[0].name;
-    let graphSelector = new GraphSelectorBuilder().namespace(namespace);
-    let graphType: GraphType = GraphType.APP;
-
-    switch (this.props.dataSource.fetchParameters.node!.nodeType) {
-      case NodeType.AGGREGATE:
-        graphSelector = graphSelector
-          .aggregate(
-            this.props.dataSource.fetchParameters.node!.aggregate!,
-            this.props.dataSource.fetchParameters.node!.aggregateValue!
-          )
-          .nodeType(NodeType.AGGREGATE);
-        break;
-      case NodeType.APP:
-        graphSelector = graphSelector.app(this.props.dataSource.fetchParameters.node!.app).nodeType(NodeType.APP);
-        break;
-      case NodeType.SERVICE:
-        graphType = GraphType.SERVICE;
-        graphSelector = graphSelector.service(this.props.dataSource.fetchParameters.node!.service);
-        break;
-      case NodeType.WORKLOAD:
-        graphType = GraphType.WORKLOAD;
-        graphSelector = graphSelector.workload(this.props.dataSource.fetchParameters.node!.workload);
-        break;
-      default:
-        // NodeType.BOX is n/a
-        break;
-    }
-
-    const graphUrl = `/graph/namespaces?graphType=${graphType}&injectServiceNodes=true&namespaces=${namespace}&focusSelector=${encodeURI(
-      graphSelector.build()
-    )}`;
-
-    if (isParentKiosk(this.props.kiosk)) {
-      kioskContextMenuAction(graphUrl);
-    } else {
-      router.navigate(graphUrl);
-    }
-  };
-
-  private onViewNodeGraph = (): void => {
-    let graphType = this.props.dataSource.fetchParameters.graphType;
-    switch (this.props.dataSource.fetchParameters.node!.nodeType) {
-      case NodeType.APP:
-        graphType = GraphType.APP;
-        break;
-      case NodeType.SERVICE:
-        graphType = GraphType.SERVICE;
-        break;
-      case NodeType.WORKLOAD:
-        graphType = GraphType.WORKLOAD;
-        break;
-    }
-
-    const urlParams: GraphUrlParams = {
-      activeNamespaces: this.props.dataSource.fetchParameters.namespaces,
-      duration: this.props.dataSource.fetchParameters.duration,
-      edgeLabels: this.props.dataSource.fetchParameters.edgeLabels,
-      edgeMode: EdgeMode.ALL,
-      graphLayout: store.getState().graph.layout,
-      namespaceLayout: store.getState().graph.namespaceLayout,
-      graphType: graphType,
-      node: this.props.dataSource.fetchParameters.node!,
-      refreshInterval: store.getState().userSettings.refreshInterval,
-      showIdleEdges: this.props.dataSource.fetchParameters.showIdleEdges,
-      showIdleNodes: this.props.dataSource.fetchParameters.showIdleNodes,
-      showOperationNodes: this.props.dataSource.fetchParameters.showOperationNodes,
-      showServiceNodes: true,
-      showWaypoints: true,
-      trafficRates: this.props.dataSource.fetchParameters.trafficRates
-    };
-
-    // To ensure updated components get the updated URL, update the URL first and then the state
-    if (isParentKiosk(this.props.kiosk)) {
-      kioskContextMenuAction(makeNodeGraphUrlFromParams(urlParams, true));
-    } else {
-      router.navigate(makeNodeGraphUrlFromParams(urlParams, true));
-    }
-  };
-
-  private toggleTimeOptionsVisibility = (): void => {
-    this.setState(prevState => ({ isTimeOptionsOpen: !prevState.isTimeOptionsOpen }));
-  };
-}
-
-const mapStateToProps = (state: KialiAppState): ReduxProps => ({
-  kiosk: state.globalState.kiosk,
-  theme: state.globalState.theme
-});
-
-export const MiniGraphCard = connect(mapStateToProps)(MiniGraphCardComponent);
diff --git a/frontend/src/components/CytoscapeGraph/TrafficAnimation/AnimationTimerConfig.ts b/frontend/src/components/CytoscapeGraph/TrafficAnimation/AnimationTimerConfig.ts
deleted file mode 100644
index 53599be3b..000000000
--- a/frontend/src/components/CytoscapeGraph/TrafficAnimation/AnimationTimerConfig.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { EdgeAnimationSpeed } from '@patternfly/react-topology';
-import { clamp } from 'utils/MathUtils';
-
-// Some information about the mathematical problem here:
-// We try to project a set of RATE values from an upper-unbounded interval ]0, +Inf[ to a set of TIMER DELAY values in
-//    a bounded interval [40ms, 5000ms] (arbitrary bounds).
-// TIMER DELAY is the delay for dots generation in the animation. The lower it is, the more dots there are.
-//
-// The constraints are:
-// - We'd like the relation to be inversely proportional: if RATE 1 is twice as big as RATE 2, DELAY 1 must be half of DELAY 2.
-// - The animations should be quite representative of volumetry both _relatively_ and _absolutely_: _relatively_ is the
-//    previous constraint (edges compared to each other); but _absolutely_ is another thing: regardless edges compared to
-//    each other, we should have a notion of low and high traffic.
-//
-// To better understand the issue, we can imagine two rates R1 = 0.001 rps and R2 = 0.1 rps. R2 should have 100 times more
-//    dots than R1. So it's likely to be "crowded", and looks like a high volumetry if we only consider the relative
-//    relationships. But thinking in absolute, 0.1 rps is small, it shouldn't be crowded.
-
-const initialThreshold = 50;
-const maxDelay = 5000;
-const minDelay = 40;
-
-export class AnimationTimerConfig {
-  private threshold: number;
-  private scaleFactor: number;
-
-  // baseDelay: for a rate of 1 rps/bps, there will be one dot every `baseDelay` seconds.
-  constructor(private baseDelay: number) {
-    this.threshold = initialThreshold;
-    this.scaleFactor = 1;
-  }
-
-  resetCalibration() {
-    this.threshold = initialThreshold;
-    this.scaleFactor = 1;
-  }
-
-  calibrate(rate: number) {
-    // We make this.threshold grow with max rate
-    // The scale factor for this graph is updated with the changed threshold.
-    if (rate > this.threshold) {
-      this.threshold = 2 * rate;
-      this.scaleFactor = this.computeScaleFactor();
-    }
-  }
-
-  computeDelay(rate: number) {
-    if (isNaN(rate) || rate === 0) {
-      return undefined;
-    }
-    // TIMER DELAY is inversely proportional to RATE. Scale factor is used to keep as much as possible TIMER DELAY in bounds
-    const d = (1000 * this.baseDelay * this.scaleFactor) / rate;
-    // In case it's out of bounds, clamp to bounds. Only case where proportionality is broken. Should only happen for very low values.
-    return clamp(d, minDelay, maxDelay);
-  }
-
-  private computeScaleFactor() {
-    // Scale factor is how much values are upscaled compared to the initial threshold that is used with low rates
-    // Some arbitrary thresholds are used to amplify high volumetry
-    if (this.threshold > 100000) {
-      return Math.pow(this.threshold / initialThreshold, 0.9);
-    }
-    if (this.threshold > 1000) {
-      return Math.pow(this.threshold / initialThreshold, 0.95);
-    }
-    return this.threshold / initialThreshold;
-  }
-
-  computeAnimationSpeedPF(rate: number): EdgeAnimationSpeed {
-    const range = maxDelay - minDelay;
-    const chunk = range / 5; // there are 5 animation speeds for PFT
-    const delay = this.computeDelay(rate);
-    switch (true) {
-      case !delay:
-        return EdgeAnimationSpeed.none;
-      case delay < chunk:
-        return EdgeAnimationSpeed.fast;
-      case delay < 2 * chunk:
-        return EdgeAnimationSpeed.mediumFast;
-      case delay < 3 * chunk:
-        return EdgeAnimationSpeed.medium;
-      case delay < 4 * chunk:
-        return EdgeAnimationSpeed.mediumSlow;
-      default:
-        return EdgeAnimationSpeed.slow;
-    }
-  }
-}
-
-// HTTP config: 1 RPS => 1 dot every second
-export const timerConfig = new AnimationTimerConfig(1);
-// TCP config: 20 bps => 1 dot every second
-export const tcpTimerConfig = new AnimationTimerConfig(20);
diff --git a/frontend/src/components/CytoscapeGraph/TrafficAnimation/TrafficPointRenderer.ts b/frontend/src/components/CytoscapeGraph/TrafficAnimation/TrafficPointRenderer.ts
deleted file mode 100644
index a7e1c4218..000000000
--- a/frontend/src/components/CytoscapeGraph/TrafficAnimation/TrafficPointRenderer.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { Point } from '../../../utils/MathUtils';
-
-export abstract class TrafficPointRenderer {
-  abstract render(context: any, point: Point);
-}
-
-export class TrafficPointCircleRenderer extends TrafficPointRenderer {
-  readonly radius: number;
-  readonly backgroundColor: string;
-  readonly borderColor: string;
-  readonly lineWidth: number;
-
-  constructor(radius: number, backgroundColor: string, borderColor: string, lineWidth: number) {
-    super();
-    this.radius = radius;
-    this.backgroundColor = backgroundColor;
-    this.borderColor = borderColor;
-    this.lineWidth = lineWidth;
-  }
-
-  render(context: any, point: Point) {
-    context.fillStyle = this.backgroundColor;
-    context.strokeStyle = this.borderColor;
-    context.lineWidth = this.lineWidth;
-    context.beginPath();
-    context.arc(point.x, point.y, this.radius, 0, 2 * Math.PI, true);
-    context.stroke();
-    context.fill();
-  }
-}
-
-export class TrafficPointConcentricDiamondRenderer extends TrafficPointRenderer {
-  readonly outerDiamond: Diamond;
-  readonly innerDiamond: Diamond;
-
-  private static diamondPath(context: any, point: Point, diamond: Diamond) {
-    context.fillStyle = diamond.backgroundColor;
-    context.strokeStyle = diamond.borderColor;
-    context.lineWidth = diamond.lineWidth;
-    context.beginPath();
-    context.moveTo(point.x, point.y - diamond.radius);
-    context.lineTo(point.x + diamond.radius, point.y);
-    context.lineTo(point.x, point.y + diamond.radius);
-    context.lineTo(point.x - diamond.radius, point.y);
-    context.lineTo(point.x, point.y - diamond.radius);
-    context.stroke();
-    context.fill();
-  }
-
-  constructor(outerDiamond: Diamond, innerDiamond: Diamond) {
-    super();
-    this.outerDiamond = outerDiamond;
-    this.innerDiamond = innerDiamond;
-  }
-
-  render(context: any, point: Point) {
-    TrafficPointConcentricDiamondRenderer.diamondPath(context, point, this.outerDiamond);
-    TrafficPointConcentricDiamondRenderer.diamondPath(context, point, this.innerDiamond);
-  }
-}
-
-export class Diamond {
-  radius: number;
-  backgroundColor: string;
-  borderColor: string;
-  lineWidth: number;
-
-  constructor(radius: number, backgroundColor: string, borderColor: string, lineWidth: number) {
-    this.radius = radius;
-    this.backgroundColor = backgroundColor;
-    this.borderColor = borderColor;
-    this.lineWidth = lineWidth;
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/TrafficAnimation/TrafficRenderer.ts b/frontend/src/components/CytoscapeGraph/TrafficAnimation/TrafficRenderer.ts
deleted file mode 100644
index ed9fe29c6..000000000
--- a/frontend/src/components/CytoscapeGraph/TrafficAnimation/TrafficRenderer.ts
+++ /dev/null
@@ -1,534 +0,0 @@
-import { Point, clamp, quadraticBezier, linearInterpolation, distance, bezierLength } from '../../../utils/MathUtils';
-import { UnhighlightClass, HoveredClass } from '../graphs/GraphStyles';
-import { PFColorVals } from '../../Pf/PfColors';
-import {
-  TrafficPointCircleRenderer,
-  TrafficPointConcentricDiamondRenderer,
-  TrafficPointRenderer,
-  Diamond
-} from './TrafficPointRenderer';
-import { decoratedEdgeData } from '../CytoscapeGraphUtils';
-import { Protocol } from '../../../types/Graph';
-import { timerConfig, tcpTimerConfig } from './AnimationTimerConfig';
-
-// Clamp response time from min to max
-const SPEED_RESPONSE_TIME_MIN = 0;
-const SPEED_RESPONSE_TIME_MAX = 10000;
-
-// Speed to travel trough an edge
-const SPEED_RATE_MIN = 0.1;
-const SPEED_RATE_MAX = 2.0;
-
-const TCP_SPEED = 1;
-
-const BASE_LENGTH = 50;
-
-// How often paint a frame
-const FRAME_RATE = 1 / 60;
-
-enum EdgeConnectionType {
-  LINEAR,
-  CURVE,
-  LOOP
-}
-
-enum TrafficEdgeType {
-  RPS, // requests-per-second (includes http, grpc)
-  TCP, // bytes-per-second
-  NONE
-}
-
-/**
- * Returns a TrafficPointRenderer for an RPS error point
- * @param edge
- * @returns {TrafficPointRenderer}
- */
-const getTrafficPointRendererForRpsError: (edge: any) => TrafficPointRenderer = (_edge: any) => {
-  return new TrafficPointConcentricDiamondRenderer(
-    new Diamond(5, PFColorVals.White, PFColorVals.Danger, 1.0),
-    new Diamond(2, PFColorVals.Danger, PFColorVals.Danger, 1.0)
-  );
-};
-
-/**
- * Returns a TrafficPointRenderer for a RPS success point
- * @param edge
- * @returns {TrafficPointRenderer}
- */
-const getTrafficPointRendererForRpsSuccess: (edge: any) => TrafficPointRenderer = (edge: any) => {
-  return new TrafficPointCircleRenderer(2, PFColorVals.White, edge.style('line-color'), 2);
-};
-
-/**
- * Returns a TrafficPointRenderer for a Tcp point
- * @param edge
- * @returns {TrafficPointCircleRenderer}
- */
-const getTrafficPointRendererForTcp: (edge: any) => TrafficPointRenderer = (_edge: any) => {
-  return new TrafficPointCircleRenderer(1.6, PFColorVals.Black100, PFColorVals.Black500, 1);
-};
-
-/**
- * Traffic Point, it defines in an edge
- * speed - defines how fast the point is going to travel from the start to the end
- *  of the edge. Is a rate of the edge length traveled by second.
- *  1 means that the edge is traveled in exactly 1 second.
- *  0.5 is 2 seconds, 2 is half a second, etc.
- * delta - defines in what part of the edge is the point,  is a normalized number
- *  from 0 to 1, 0 means at the start of the path, and 1 is the end. The position
- *  is interpolated.
- * offset - Offset to add to the rendered point position.
- * renderer - Renderer used to draw the shape at a given position.
- */
-type TrafficPoint = {
-  speed: number;
-  delta: number;
-  offset: Point;
-  renderer: TrafficPointRenderer;
-};
-
-/**
- * Helps generate traffic points
- * timer - defines how fast to generate a new point, its in milliseconds.
- * timerForNextPoint - keeps track of how many milliseconds to generate the next point.
- * speed - defines the speed of the next point (see TrafficPoint.speed)
- */
-class TrafficPointGenerator {
-  private timer?: number;
-  private timerForNextPoint?: number;
-  private speed: number = 0;
-  private errorRate: number = 0;
-  private type: TrafficEdgeType = TrafficEdgeType.NONE;
-
-  /**
-   * Process a render step for the generator, decrements the timerForNextPoint and
-   * returns a new point if it reaches zero (or is close).
-   * This method adds some randomness to avoid the "flat" look that all the points
-   * are synchronized.
-   */
-  processStep(step: number, edge: any): TrafficPoint | undefined {
-    if (this.timerForNextPoint !== undefined) {
-      this.timerForNextPoint -= step;
-      // Add some random-ness to make it less "flat"
-      if (this.timerForNextPoint <= Math.random() * 200) {
-        this.timerForNextPoint = this.timer;
-        return this.nextPoint(edge);
-      }
-    }
-    return undefined;
-  }
-
-  setTimer(timer: number | undefined) {
-    this.timer = timer;
-    // Start as soon as posible, unless we have no traffic
-    if (this.timerForNextPoint === undefined) {
-      this.timerForNextPoint = timer;
-    }
-  }
-
-  setSpeed(speed: number) {
-    this.speed = speed;
-  }
-
-  setErrorRate(errorRate: number) {
-    this.errorRate = errorRate;
-  }
-
-  setType(type: TrafficEdgeType) {
-    this.type = type;
-  }
-
-  private nextPoint(edge: any): TrafficPoint {
-    let renderer;
-    let offset;
-    const isErrorPoint = Math.random() <= this.errorRate;
-    if (this.type === TrafficEdgeType.RPS) {
-      renderer = isErrorPoint ? getTrafficPointRendererForRpsError(edge) : getTrafficPointRendererForRpsSuccess(edge);
-    } else if (this.type === TrafficEdgeType.TCP) {
-      renderer = getTrafficPointRendererForTcp(edge);
-      // Cheap way to put some offset around the edge, I think this is enough unless we want more accuracy
-      // More accuracy would need to identify the slope of current segment of the edgge (for curves and loops) to only do
-      // offsets perpendicular to it, instead of it, we are moving around a circle area
-      // Random offset (x,y); 'x' in [-1.5, 1.5] and 'y' in [-1.5, 1.5]
-      offset = { x: Math.random() * 3 - 1.5, y: Math.random() * 3 - 1.5 };
-    }
-
-    return {
-      speed: this.speed,
-      delta: 0, // at the beginning of the edge
-      renderer: renderer,
-      offset: offset
-    };
-  }
-}
-
-/**
- * Holds the list of points an edge has.
- * points - list of active points the edge has, points are discarded when they
- *  reach their target.
- * generator - Generates the next point
- * edge - Edge where the traffic is tracked
- */
-class TrafficEdge {
-  private points: Array<TrafficPoint> = [];
-  private generator: TrafficPointGenerator;
-  private edge: any;
-  private type: TrafficEdgeType = TrafficEdgeType.NONE;
-
-  constructor() {
-    this.generator = new TrafficPointGenerator();
-  }
-
-  /**
-   * Process a step for the Traffic Edge, increments the delta of the points
-   * Calls `processStep` for the generator and adds a new point if any.
-   */
-  processStep(step: number) {
-    this.points = this.points.map(p => {
-      p.delta += (step * p.speed) / 1000;
-      return p;
-    });
-    const point = this.generator.processStep(step, this.edge);
-    if (point) {
-      this.points.push(point);
-    }
-  }
-
-  getPoints() {
-    return this.points;
-  }
-
-  getEdge() {
-    return this.edge;
-  }
-
-  getType() {
-    return this.type;
-  }
-
-  setTimer(timer: number | undefined) {
-    this.generator.setTimer(timer);
-  }
-
-  /**
-   * When a point is 1 or over it, is time to discard it.
-   */
-  removeFinishedPoints() {
-    this.points = this.points.filter(p => p.delta <= 1);
-  }
-
-  setSpeed(speed: number) {
-    this.generator.setSpeed(speed);
-  }
-
-  setErrorRate(errorRate: number) {
-    this.generator.setErrorRate(errorRate);
-  }
-
-  setEdge(edge: any) {
-    this.edge = edge;
-  }
-
-  setType(type: TrafficEdgeType) {
-    this.type = type;
-    this.generator.setType(type);
-  }
-}
-
-type TrafficEdgeHash = {
-  [edgeId: string]: TrafficEdge;
-};
-
-/**
- * Renders the traffic going from edges using the edge information to compute
- * their rate and speed
- *
- * rate determines how often to put a TrafficPoint in the edge.
- * responseTime determines how fast the TrafficPoint should travel from the start to the end of the edge.
- * percentErr determine if the next TrafficPoint is error or not.
- */
-export class TrafficRenderer {
-  private animationTimer;
-  private previousTimestamp;
-  private trafficEdges: TrafficEdgeHash = {};
-
-  private readonly layer;
-  private readonly context;
-
-  constructor(cy: any) {
-    this.layer = cy.cyCanvas();
-    const canvas = this.layer.getCanvas();
-    canvas.style['pointer-events'] = 'none';
-    this.context = canvas.getContext('2d');
-  }
-
-  /**
-   * Starts the rendering loop, discards any other rendering loop that was started
-   */
-  start(edges: any) {
-    this.pause();
-    this.trafficEdges = this.processEdges(edges);
-    this.animationTimer = window.setInterval(this.processStep, FRAME_RATE * 1000);
-  }
-
-  /**
-   * Stops the rendering loop if any
-   */
-  pause() {
-    if (this.animationTimer !== undefined) {
-      window.clearInterval(this.animationTimer);
-      this.layer.clear(this.context);
-      this.animationTimer = undefined;
-      this.previousTimestamp = undefined;
-    }
-  }
-
-  /**
-   * Stops the rendering loop if any
-   */
-  stop() {
-    this.pause();
-    this.trafficEdges = {};
-  }
-
-  /**
-   * Process a step, clears the canvas, sets the graph transformation to render
-   * every dot.
-   */
-  processStep = () => {
-    try {
-      const nextTimestamp = Date.now();
-      if (!this.previousTimestamp) {
-        this.previousTimestamp = nextTimestamp;
-      }
-      const step = this.currentStep(nextTimestamp);
-      this.layer.clear(this.context);
-      this.layer.setTransform(this.context);
-      Object.keys(this.trafficEdges).forEach(edgeId => {
-        const trafficEdge = this.trafficEdges[edgeId];
-        const edge = trafficEdge.getEdge();
-        // Skip if edge is currently hidden or removed
-        if (edge.visible() && edge.inside()) {
-          trafficEdge.processStep(step);
-          trafficEdge.removeFinishedPoints();
-          this.render(trafficEdge);
-        }
-      });
-      this.previousTimestamp = nextTimestamp;
-    } catch (exception) {
-      // If a step failed, the next step is likely to fail.
-      // Stop the rendering and throw the exception
-      this.stop();
-      throw exception;
-    }
-  };
-
-  /**
-   * Renders the points inside the TrafficEdge (unless unhighlighted)
-   *
-   */
-  private render(trafficEdge: TrafficEdge) {
-    const edge = trafficEdge.getEdge();
-    if (edge.hasClass(UnhighlightClass) || edge.hasClass(HoveredClass)) {
-      return;
-    }
-    trafficEdge.getPoints().forEach((point: TrafficPoint) => {
-      const controlPoints = this.edgeControlPoints(edge);
-      try {
-        const pointInGraph = this.pointWithOffset(this.pointInGraph(controlPoints, point.delta), point.offset);
-
-        if (pointInGraph) {
-          point.renderer.render(this.context, pointInGraph);
-        }
-      } catch (error) {
-        if (error instanceof Error) {
-          console.error(`Error rendering TrafficEdge, it won't be rendered: ${error.message}`);
-        }
-      }
-    });
-  }
-
-  private pointInGraph(controlPoints: Array<Point>, t: number) {
-    /*
-     * Control points are build so that if you have p0, p1, p2, p3, p4 points, you need to build 2 quadratic bezier:
-     * 1) p0 (t=0), p1 (t=0.5) and p2 (t=1) and 2) p2 (t=0), p3 (t=0.5) and p4 (t=1)
-     * p0 and p4 (or pn) are always the source and target of an edge.
-     * Commonly there is only 2 points for straight lines, 3  points for curves and 5 points for loops.
-     * Not going to generalize them now to avoid having a more complex code that is needed.
-     * https://github.com/cytoscape/cytoscape.js/issues/2139#issuecomment-398473432
-     */
-    const edgeConnectionType = this.edgeConnectionTypeFromControlPoints(controlPoints);
-    switch (edgeConnectionType) {
-      case EdgeConnectionType.LINEAR:
-        return linearInterpolation(controlPoints[0], controlPoints[1], t);
-      case EdgeConnectionType.CURVE:
-        return quadraticBezier(controlPoints[0], controlPoints[1], controlPoints[2], t);
-      case EdgeConnectionType.LOOP:
-        // Find the local t depending the current step
-        if (t < 0.5) {
-          // Normalize [0, 0.5)
-          return quadraticBezier(controlPoints[0], controlPoints[1], controlPoints[2], t / 0.5);
-        } else {
-          // Normalize [0.5, 1]
-          return quadraticBezier(controlPoints[2], controlPoints[3], controlPoints[4], (t - 0.5) * 2);
-        }
-      default:
-        throw Error('Unhandled EdgeConnectionType:' + edgeConnectionType);
-    }
-  }
-
-  private pointWithOffset(point: Point, offset: Point) {
-    return offset === undefined ? point : { x: point.x + offset.x, y: point.y + offset.y };
-  }
-
-  private currentStep(currentTime: number): number {
-    const step = currentTime - this.previousTimestamp;
-    return step === 0 ? FRAME_RATE * 1000 : step;
-  }
-
-  private getTrafficEdgeType(edge: any) {
-    switch (decoratedEdgeData(edge).protocol) {
-      case Protocol.GRPC:
-      case Protocol.HTTP:
-        return TrafficEdgeType.RPS;
-      case Protocol.TCP:
-        return TrafficEdgeType.TCP;
-      default:
-        return TrafficEdgeType.NONE;
-    }
-  }
-
-  private processEdges(edges: any): TrafficEdgeHash {
-    timerConfig.resetCalibration();
-    tcpTimerConfig.resetCalibration();
-    // Calibrate animation amplitude
-    edges.forEach(edge => {
-      const edgeData = decoratedEdgeData(edge);
-      switch (edgeData.protocol) {
-        case Protocol.GRPC:
-          timerConfig.calibrate(edgeData.grpc);
-          break;
-        case Protocol.HTTP:
-          timerConfig.calibrate(edgeData.http);
-          break;
-        case Protocol.TCP:
-          tcpTimerConfig.calibrate(edgeData.tcp);
-          break;
-      }
-    });
-    // Process edges
-    return edges.reduce((trafficEdges: TrafficEdgeHash, edge: any) => {
-      const type = this.getTrafficEdgeType(edge);
-      if (type !== TrafficEdgeType.NONE) {
-        const edgeId = decoratedEdgeData(edge).id;
-        if (edgeId in this.trafficEdges) {
-          trafficEdges[edgeId] = this.trafficEdges[edgeId];
-        } else {
-          trafficEdges[edgeId] = new TrafficEdge();
-        }
-        trafficEdges[edgeId].setType(type);
-        this.fillTrafficEdge(edge, trafficEdges[edgeId]);
-      }
-      return trafficEdges;
-    }, {});
-  }
-
-  private fillTrafficEdge(edge: any, trafficEdge: TrafficEdge) {
-    // Need to identify if we are going to fill an RPS or TCP traffic edge
-    // RPS traffic has rate, responseTime, percentErr (among others) where TCP traffic only has: tcpSentRate
-
-    let edgeLengthFactor = 1;
-    try {
-      const edgeLength = this.edgeLength(edge);
-      edgeLengthFactor = BASE_LENGTH / Math.max(edgeLength, 1);
-    } catch (error) {
-      if (error instanceof Error) {
-        console.error(
-          `Error when finding the length of the edge for the traffic animation, this TrafficEdge won't be rendered: ${error.message}`
-        );
-      }
-    }
-
-    const edgeData = decoratedEdgeData(edge);
-    if (trafficEdge.getType() === TrafficEdgeType.RPS) {
-      const isHttp = edgeData.protocol === Protocol.HTTP;
-      const rate = isHttp ? edgeData.http : edgeData.grpc;
-      const pErr = isHttp ? edgeData.httpPercentErr : edgeData.grpcPercentErr;
-
-      const timer = timerConfig.computeDelay(rate);
-      // The edge of the length also affects the speed, include a factor in the speed to even visual speed for
-      // long and short edges.
-      const speed = this.speedFromResponseTime(edgeData.responseTime) * edgeLengthFactor;
-      const errorRate = isNaN(pErr) ? 0 : pErr / 100;
-      trafficEdge.setSpeed(speed);
-      trafficEdge.setTimer(timer);
-      trafficEdge.setEdge(edge);
-      trafficEdge.setErrorRate(errorRate);
-    } else if (trafficEdge.getType() === TrafficEdgeType.TCP) {
-      trafficEdge.setSpeed(TCP_SPEED * edgeLengthFactor);
-      trafficEdge.setErrorRate(0);
-      trafficEdge.setTimer(tcpTimerConfig.computeDelay(edgeData.tcp));
-      trafficEdge.setEdge(edge);
-    }
-  }
-
-  private speedFromResponseTime(responseTime: number) {
-    // Consider NaN response time as "everything is going as fast as possible"
-    if (isNaN(responseTime)) {
-      return SPEED_RATE_MAX;
-    }
-    // Normalize
-    const delta = clamp(responseTime, SPEED_RESPONSE_TIME_MIN, SPEED_RESPONSE_TIME_MAX) / SPEED_RESPONSE_TIME_MAX;
-    // Scale
-    return SPEED_RATE_MIN + (1 - delta) * (SPEED_RATE_MAX - SPEED_RATE_MIN);
-  }
-
-  private edgeLength(edge: any) {
-    const controlPoints = this.edgeControlPoints(edge);
-    const edgeConnectionType = this.edgeConnectionTypeFromControlPoints(controlPoints);
-    switch (edgeConnectionType) {
-      case EdgeConnectionType.LINEAR:
-        return distance(controlPoints[0], controlPoints[1]);
-      case EdgeConnectionType.CURVE:
-        return bezierLength(controlPoints[0], controlPoints[1], controlPoints[2]);
-      case EdgeConnectionType.LOOP:
-        return (
-          bezierLength(controlPoints[0], controlPoints[1], controlPoints[2]) +
-          bezierLength(controlPoints[2], controlPoints[3], controlPoints[4])
-        );
-      default:
-        throw Error('Unhandled EdgeConnectionType:' + edgeConnectionType);
-    }
-  }
-
-  private edgeControlPoints(edge: any) {
-    const controlPoints: Array<Point> = [edge.sourceEndpoint()];
-    const rawControlPoints = edge.controlPoints();
-    if (rawControlPoints) {
-      for (let i = 0; i < rawControlPoints.length; ++i) {
-        controlPoints.push(rawControlPoints[i]);
-        // If there is a next point, we are going to use the midpoint for the next point
-        if (i + 1 < rawControlPoints.length) {
-          controlPoints.push({
-            x: (rawControlPoints[i].x + rawControlPoints[i + 1].x) / 2,
-            y: (rawControlPoints[i].y + rawControlPoints[i + 1].y) / 2
-          });
-        }
-      }
-    }
-    controlPoints.push(edge.targetEndpoint());
-    return controlPoints;
-  }
-
-  private edgeConnectionTypeFromControlPoints(controlPoints: Array<Point>) {
-    if (controlPoints.length === 2) {
-      return EdgeConnectionType.LINEAR;
-    } else if (controlPoints.length === 3) {
-      return EdgeConnectionType.CURVE;
-    } else if (controlPoints.length === 5) {
-      return EdgeConnectionType.LOOP;
-    } else {
-      throw Error('Unknown EdgeConnectionType, ControlPoint.length=' + controlPoints.length);
-    }
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/TrafficAnimation/__tests__/AnimationTimerConfig.test.ts b/frontend/src/components/CytoscapeGraph/TrafficAnimation/__tests__/AnimationTimerConfig.test.ts
deleted file mode 100644
index 16c68bfdb..000000000
--- a/frontend/src/components/CytoscapeGraph/TrafficAnimation/__tests__/AnimationTimerConfig.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { AnimationTimerConfig } from '../AnimationTimerConfig';
-
-describe('AnimationTimeConfig', () => {
-  it('should produce baseDelay*1000 with rate 1', () => {
-    let baseDelay = 1;
-    let timerConfig = new AnimationTimerConfig(baseDelay);
-    timerConfig.resetCalibration();
-    timerConfig.calibrate(1);
-    let delay = timerConfig.computeDelay(1);
-    expect(delay).toEqual(1000 * baseDelay);
-
-    baseDelay = 4;
-    timerConfig = new AnimationTimerConfig(baseDelay);
-    timerConfig.resetCalibration();
-    timerConfig.calibrate(1);
-    delay = timerConfig.computeDelay(1);
-    expect(delay).toEqual(1000 * baseDelay);
-  });
-
-  it('should scale inversely proportional', () => {
-    const timerConfig = new AnimationTimerConfig(4);
-    const rates = [10, 20, 30];
-    timerConfig.resetCalibration();
-    rates.forEach(r => timerConfig.calibrate(r));
-    const [d1, d2, d3] = rates.map(r => timerConfig.computeDelay(r));
-    expect(d1).toEqual(2 * d2);
-    expect(d1).toEqual(3 * d3);
-  });
-
-  it('should clamp low rates', () => {
-    const timerConfig = new AnimationTimerConfig(4);
-    const rates = [0.001, 0.01, 0.1, 1];
-    timerConfig.resetCalibration();
-    rates.forEach(r => timerConfig.calibrate(r));
-    const [d1, d2, d3, d4] = rates.map(r => timerConfig.computeDelay(r));
-    // Unit: 1 => baseDelay * 1000
-    expect(d4).toEqual(4000);
-    // Others reach max 5000 => clamped
-    expect(d1).toEqual(5000);
-    expect(d2).toEqual(5000);
-    expect(d3).toEqual(5000);
-  });
-
-  it('should be kept in bounds with high rates', () => {
-    const timerConfig = new AnimationTimerConfig(4);
-    const rates = [1, 100, 1000, 2000];
-    timerConfig.resetCalibration();
-    rates.forEach(r => timerConfig.calibrate(r));
-    const [d1, d2, d3, d4] = rates.map(r => timerConfig.computeDelay(r));
-    // Due to upscaling, unit 1 is now out of bounds, so clamped to 5000
-    expect(d1).toEqual(5000);
-    // Others are within bounds, so still proportional to each other
-    expect(d3).toEqual(d2 / 10);
-    expect(d4).toEqual(d2 / 20);
-  });
-});
diff --git a/frontend/src/components/CytoscapeGraph/__tests__/CytoscapeGraph.test.tsx b/frontend/src/components/CytoscapeGraph/__tests__/CytoscapeGraph.test.tsx
deleted file mode 100644
index 445230e91..000000000
--- a/frontend/src/components/CytoscapeGraph/__tests__/CytoscapeGraph.test.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import * as React from 'react';
-import { shallow } from 'enzyme';
-
-import { CytoscapeGraph } from '../CytoscapeGraph';
-import * as GRAPH_DATA from '../../../services/__mockData__/getGraphElements';
-import { DefaultTrafficRates, EdgeLabelMode, EdgeMode, GraphType, Layout } from '../../../types/Graph';
-import { EmptyGraphLayout } from '../EmptyGraphLayout';
-import { decorateGraphData } from '../../../store/Selectors/GraphData';
-import { GraphDataSource } from '../../../services/GraphDataSource';
-import { toSafeCyFieldName } from '../CytoscapeGraphUtils';
-import { Theme } from 'types/Common';
-
-jest.mock('../../../services/Api');
-
-const testNamespace = 'ISTIO_SYSTEM';
-
-const testClickHandler = (): void => {
-  console.log('click');
-};
-
-const testReadyHandler = (): void => {
-  console.log('ready');
-};
-
-const testSetHandler = (): void => {
-  console.log('set');
-};
-
-describe('CytoscapeGraph component test', () => {
-  it('should set correct elements data', done => {
-    const myLayout: Layout = { name: 'breadthfirst' };
-    const nsLayout: Layout = { name: 'kiali-dagre' };
-    const myEdgeLabelMode: EdgeLabelMode[] = [];
-
-    const dataSource = new GraphDataSource();
-    dataSource.fetchGraphData({
-      duration: 60,
-      edgeLabels: myEdgeLabelMode,
-      graphType: GraphType.VERSIONED_APP,
-      includeHealth: false,
-      includeLabels: false,
-      injectServiceNodes: true,
-      namespaces: [{ name: testNamespace }],
-      queryTime: 0,
-      showIdleEdges: false,
-      showIdleNodes: false,
-      showOperationNodes: false,
-      showSecurity: true,
-      showWaypoints: false,
-      trafficRates: DefaultTrafficRates
-    });
-
-    dataSource.on('fetchSuccess', () => {
-      const wrapper = shallow(
-        <CytoscapeGraph
-          edgeLabels={myEdgeLabelMode}
-          edgeMode={EdgeMode.ALL}
-          graphData={{
-            elements: dataSource.graphData,
-            elementsChanged: true,
-            isLoading: false,
-            fetchParams: {
-              duration: 60,
-              edgeLabels: myEdgeLabelMode,
-              graphType: GraphType.VERSIONED_APP,
-              includeHealth: false,
-              includeLabels: false,
-              injectServiceNodes: true,
-              namespaces: [{ name: testNamespace }],
-              queryTime: 0,
-              showIdleEdges: false,
-              showIdleNodes: false,
-              showOperationNodes: false,
-              showSecurity: true,
-              showWaypoints: false,
-              trafficRates: DefaultTrafficRates
-            },
-            timestamp: 0
-          }}
-          layout={myLayout}
-          namespaceLayout={nsLayout}
-          updateSummary={testClickHandler}
-          onReady={testReadyHandler}
-          onEmptyGraphAction={testClickHandler}
-          refreshInterval={0}
-          setActiveNamespaces={testSetHandler}
-          setNode={testSetHandler}
-          rankBy={[]}
-          showIdleEdges={false}
-          showIdleNodes={false}
-          showOutOfMesh={true}
-          showOperationNodes={false}
-          showRank={false}
-          showSecurity={true}
-          showServiceNodes={true}
-          showTrafficAnimation={false}
-          showVirtualServices={true}
-          summaryData={null}
-          toggleIdleNodes={() => undefined}
-          theme={Theme.LIGHT}
-        />
-      );
-
-      const emptyGraphLayoutWrapper = wrapper.find(EmptyGraphLayout);
-      const emptyGraphDecorated = decorateGraphData(GRAPH_DATA[testNamespace].elements, 60);
-      expect(emptyGraphLayoutWrapper.prop('elements')!.nodes).toEqual(emptyGraphDecorated.nodes);
-      expect(emptyGraphLayoutWrapper.prop('elements')!.edges).toEqual(emptyGraphDecorated.edges);
-
-      done();
-    });
-  });
-
-  describe('utils test', () => {
-    it('should have working utils', done => {
-      expect(toSafeCyFieldName('foo')).toEqual('foo');
-      expect(toSafeCyFieldName('label_foo')).toEqual('label_foo');
-      expect(toSafeCyFieldName('label:kiali.io/foo')).toEqual('label_kiali_io_foo');
-
-      done();
-    });
-  });
-});
diff --git a/frontend/src/components/CytoscapeGraph/__tests__/CytoscapeGraphSelector.test.ts b/frontend/src/components/CytoscapeGraph/__tests__/CytoscapeGraphSelector.test.ts
deleted file mode 100644
index ef1f4e098..000000000
--- a/frontend/src/components/CytoscapeGraph/__tests__/CytoscapeGraphSelector.test.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { GraphSelectorBuilder } from 'pages/Graph/GraphSelector';
-import { NodeType, BoxByType } from '../../../types/Graph';
-
-describe('GraphSelector test', () => {
-  it('Generates selector for app', () => {
-    const selector = new GraphSelectorBuilder().app('myapp').build();
-    expect(selector).toEqual('node[app="myapp"]');
-  });
-
-  it('Generates selector for id', () => {
-    const selector = new GraphSelectorBuilder().id('myid').build();
-    expect(selector).toEqual('node[id="myid"]');
-  });
-
-  it('Generates selector for namespace', () => {
-    const selector = new GraphSelectorBuilder().namespace('mynamespace').build();
-    expect(selector).toEqual('node[namespace="mynamespace"]');
-  });
-
-  it('Generates selector for nodeType', () => {
-    const selector = new GraphSelectorBuilder().nodeType(NodeType.APP).build();
-    expect(selector).toEqual('node[nodeType="app"]');
-  });
-
-  it('Generates selector for service', () => {
-    const selector = new GraphSelectorBuilder().service('myservice').build();
-    expect(selector).toEqual('node[service="myservice"]');
-  });
-
-  it('Generates selector for version', () => {
-    const selector = new GraphSelectorBuilder().version('myversion').build();
-    expect(selector).toEqual('node[version="myversion"]');
-  });
-
-  it('Generates selector for workload', () => {
-    const selector = new GraphSelectorBuilder().workload('myworkload').build();
-    expect(selector).toEqual('node[workload="myworkload"]');
-  });
-
-  it('Generates selector for isBox', () => {
-    const selector = new GraphSelectorBuilder().isBox(BoxByType.APP).build();
-    expect(selector).toEqual('node[isBox="app"]');
-  });
-
-  it('Generates falsy selector for isBox', () => {
-    const selector = new GraphSelectorBuilder().isBox(null).build();
-    expect(selector).toEqual('node[!isBox]');
-  });
-
-  it('Generates selector for two properties', () => {
-    const selector = new GraphSelectorBuilder().workload('myworkload').app('myapp').build();
-    expect(selector).toEqual('node[workload="myworkload"][app="myapp"]');
-  });
-
-  it('Generates selector for multiple properties', () => {
-    const selector = new GraphSelectorBuilder()
-      .workload('myworkload')
-      .id('myid')
-      .version('myversion')
-      .service('myservice')
-      .build();
-    expect(selector).toEqual('node[workload="myworkload"][id="myid"][version="myversion"][service="myservice"]');
-  });
-});
diff --git a/frontend/src/components/CytoscapeGraph/graphs/GraphHighlighter.ts b/frontend/src/components/CytoscapeGraph/graphs/GraphHighlighter.ts
deleted file mode 100644
index 8c37906cc..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/GraphHighlighter.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { BoxByType, CytoscapeBaseEvent, NodeAttr } from '../../../types/Graph';
-import { UnhighlightClass, HoveredClass, HighlightClass } from './GraphStyles';
-
-// There are three special states for a Node or Edge:
-//
-//   - selected: The node has been single-clicked and is 'selected' in cytoscape
-//   - hovered: The mouse is currently over the element.  It is marked with HoveredClass
-//   - highlighted: The element is to be emphasized (see below).  It is marked with HighlightClass.
-//   - unhighlighted: The element is to be de-emphasized (see below).  It is marked with UnhighlightClass.
-//
-// When a node or edge is hovered or selected:
-//   - highlight the end-to-end paths (nodes and edges) for which the element may participate.
-//     - note, this is not trace-based, so a highlighted path may not represent an actual request path
-//   - unhighlight the remaining elements
-//   - hovering is ignored while an element is selected
-//
-// When a box is selected:
-//   - highlight the contained nodes and their related nodes (including edges).
-//
-// When a boxed node is highlighted it's parent boxes, if any, are highlighted as well.
-//
-// Note that this code is responsible only for maintaining the element class assignments, the actual
-// visualization changes, given the class assignments, is up to GraphStyles.ts.
-//
-export class GraphHighlighter {
-  cy: any;
-  selected: CytoscapeBaseEvent;
-  hovered?: CytoscapeBaseEvent;
-
-  constructor(cy: any) {
-    this.cy = cy;
-    this.selected = {
-      summaryType: 'graph',
-      summaryTarget: this.cy
-    };
-  }
-
-  // Need to define these methods using the "public class fields syntax", to be able to keep
-  // *this* binded when passing it to events handlers (or use the annoying syntax)
-  // https://reactjs.org/docs/handling-events.html
-  onClick = (event: CytoscapeBaseEvent) => {
-    // ignore clicks on the currently selected element
-    if (this.selected.summaryTarget === event.summaryTarget) {
-      return;
-    }
-
-    this.selected = event;
-    this.clearHover();
-    this.clearHighlighting();
-
-    // only highlight when selecting something other than the graph background
-    if (this.selected.summaryType !== 'graph') {
-      this.refresh();
-    }
-  };
-
-  clearHover = () => {
-    if (this.hovered) {
-      this.hovered.summaryTarget.removeClass(HoveredClass);
-      this.hovered = undefined;
-    }
-  };
-
-  onMouseIn = (event: CytoscapeBaseEvent) => {
-    // only set Hovered when the graph is currently selected, otherwise just leave the selected element as-is
-    if (this.selected.summaryType === 'graph' && ['node', 'edge', 'box'].includes(event.summaryType)) {
-      this.hovered = event;
-      this.hovered.summaryTarget.addClass(HoveredClass);
-      this.refresh();
-    }
-  };
-
-  onMouseOut = (event: CytoscapeBaseEvent) => {
-    if (this.hovered && this.hovered.summaryTarget === event.summaryTarget) {
-      this.clearHover();
-      this.clearHighlighting();
-    }
-  };
-
-  clearHighlighting = () => {
-    this.cy.elements(`.${UnhighlightClass}`).removeClass(UnhighlightClass);
-    this.cy.elements(`.${HighlightClass}`).removeClass(HighlightClass);
-  };
-
-  refresh = () => {
-    const highlighted = this.getHighlighted();
-    if (!highlighted.toHighlight) {
-      return;
-    }
-
-    highlighted.toHighlight.addClass(HighlightClass);
-
-    if (highlighted.unhighlightOthers) {
-      this.cy.elements().difference(highlighted.toHighlight).addClass(UnhighlightClass);
-    }
-  };
-
-  // Returns the nodes to highlight. Highlighting for a hovered or selected element
-  // is the same, it extends to full inbound and outbound paths.
-  getHighlighted(): { toHighlight: any; unhighlightOthers: boolean } {
-    const isHover = this.selected.summaryType === 'graph';
-    const event = isHover ? this.hovered : this.selected;
-    if (event) {
-      switch (event.summaryType) {
-        case 'node':
-          return { toHighlight: this.getNodeHighlight(event.summaryTarget, false), unhighlightOthers: true };
-        case 'edge':
-          return { toHighlight: this.getEdgeHighlight(event.summaryTarget, false), unhighlightOthers: true };
-        case 'box':
-          return this.getBoxHighlight(event.summaryTarget, false);
-        default:
-        // fall through
-      }
-    }
-    return { toHighlight: undefined, unhighlightOthers: false };
-  }
-
-  includeAncestorNodes(nodes: any) {
-    return nodes.reduce((all, current) => {
-      all = all.add(current);
-      if (current.isChild()) {
-        all = all.add(current.ancestors());
-      }
-      return all;
-    }, this.cy.collection());
-  }
-
-  getNodeHighlight(node: any, neighborhoodOnly: boolean) {
-    const elems = neighborhoodOnly ? node.closedNeighborhood() : node.predecessors().add(node.successors());
-    return this.includeAncestorNodes(elems.add(node));
-  }
-
-  getEdgeHighlight(edge: any, neighborhoodOnly: boolean) {
-    const source = edge.source();
-    const target = edge.target();
-    let elems = source.add(target);
-
-    if (!neighborhoodOnly) {
-      elems = elems.add(source.predecessors()).add(target.successors());
-    }
-    return this.includeAncestorNodes(elems.add(edge));
-  }
-
-  getBoxHighlight(box: any, neighborhoodOnly: boolean): { toHighlight: any; unhighlightOthers: boolean } {
-    // treat App boxes in a typical way, but to reduce "flashing", Namespace and Cluster
-    // boxes highlight themselves and their anscestors.
-    if (box.data(NodeAttr.isBox) === BoxByType.APP) {
-      let elems;
-      if (neighborhoodOnly) {
-        elems = box.descendants().reduce((prev, child) => {
-          return prev.add(child.closedNeighborhood());
-        }, this.cy.collection());
-      } else {
-        const children = box.descendants();
-        elems = children.add(children.predecessors()).add(children.successors());
-      }
-      return { toHighlight: this.includeAncestorNodes(elems), unhighlightOthers: true };
-    }
-    return { toHighlight: this.includeAncestorNodes(box), unhighlightOthers: false };
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/graphs/GraphStyles.ts b/frontend/src/components/CytoscapeGraph/graphs/GraphStyles.ts
deleted file mode 100644
index 343023fbc..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/GraphStyles.ts
+++ /dev/null
@@ -1,1008 +0,0 @@
-import { kialiStyle } from 'styles/StyleUtils';
-import { PFColors, PFColorVal, PFColorVals } from '../../../components/Pf/PfColors';
-import { DEGRADED, FAILURE } from '../../../types/Health';
-import {
-  BoxByType,
-  CytoscapeGlobalScratchData,
-  CytoscapeGlobalScratchNamespace,
-  EdgeLabelMode,
-  GraphType,
-  NodeAttr,
-  NodeType,
-  numLabels,
-  Protocol,
-  TrafficRate,
-  UNKNOWN
-} from '../../../types/Graph';
-import { icons, serverConfig } from '../../../config';
-import NodeImageTopology from '../../../assets/img/graph/node-background-topology.png';
-import NodeImageKey from '../../../assets/img/graph/node-background-key.png';
-import { decoratedEdgeData, decoratedNodeData } from '../CytoscapeGraphUtils';
-import _ from 'lodash';
-import * as Cy from 'cytoscape';
-import { PFBadges } from 'components/Pf/PfBadges';
-import { config } from 'config/Config';
-import { kialiBadge, PFBadgeType } from '../../Pf/PfBadges';
-import { NestedCSSProperties } from 'typestyle/lib/types';
-
-export const HighlightClass = 'mousehighlight';
-export const HoveredClass = 'mousehover';
-export const UnhighlightClass = 'mouseunhighlight';
-
-let EdgeColor: PFColorVal;
-let EdgeColorDead: PFColorVal;
-let EdgeColorDegraded: PFColorVal;
-let EdgeColorFailure: PFColorVal;
-let EdgeColorTCPWithTraffic: PFColorVal;
-const EdgeIconMTLS = icons.istio.mtls.ascii; // lock
-let EdgeTextOutlineColor: PFColorVal;
-const EdgeTextOutlineWidth = '1px';
-const EdgeTextFont = 'Verdana,Arial,Helvetica,sans-serif,pficon';
-const EdgeWidth = 2;
-const EdgeWidthSelected = 4;
-const FontSizeRatioEdgeText = 0.8;
-const FontSizeRatioHover = 1.2;
-const FontSizeRatioHoverBox = 1.3;
-const NodeBorderWidth = '1px';
-const NodeBorderWidthSelected = '3px';
-let NodeColorBorder: PFColorVal;
-let NodeColorBorderBox: PFColorVal;
-let NodeColorBorderDegraded: PFColorVal;
-let NodeColorBorderFailure: PFColorVal;
-let NodeColorBorderHover: PFColorVal;
-let NodeColorBorderSelected: PFColorVal;
-let NodeColorFill: PFColorVal;
-let NodeColorFillBoxApp: PFColorVal;
-let NodeColorFillBoxCluster: PFColorVal;
-let NodeColorFillBoxNamespace: PFColorVal;
-let NodeColorFillHover: PFColorVal;
-let NodeColorFillHoverDegraded: PFColorVal;
-let NodeColorFillHoverFailure: PFColorVal;
-const NodeHeight = '25px';
-const NodeIconCB = icons.istio.circuitBreaker.className; // bolt
-const NodeIconFaultInjection = icons.istio.faultInjection.className; // ban
-const NodeIconGateway = icons.istio.gateway.className; // globe
-const NodeIconMirroring = icons.istio.mirroring.className; // migration
-const NodeIconMS = icons.istio.missingSidecar.className; // blueprint
-const NodeIconRoot = icons.istio.root.className; // alt-arrow-circle-right
-const NodeIconVS = icons.istio.virtualService.className; // code-branch
-const NodeIconRequestRouting = icons.istio.requestRouting.className; // code-branch
-const NodeIconRequestTimeout = icons.istio.requestTimeout.className; // clock
-const NodeIconTrafficShifting = icons.istio.trafficShifting.className; // share-alt
-const NodeIconWorkloadEntry = icons.istio.workloadEntry.className; // pf-icon-virtual-machine
-const NodeIconWaypoint = icons.istio.waypoint.className;
-const NodeTextColor = PFColors.Black1000;
-const NodeTextColorBox = PFColors.Black1000;
-const NodeTextBackgroundColor = PFColors.White;
-const NodeTextBackgroundColorBox = PFColors.Black200;
-const NodeBadgeBackgroundColor = PFColors.Purple500;
-const NodeBadgeColor = PFColors.White;
-const NodeTextFont = EdgeTextFont;
-const NodeWidth = NodeHeight;
-const OpacityOverlay = 0.3;
-const OpacityUnhighlightLeast = 0.5;
-const OpacityUnhighlightMedium = 0.3;
-const OpacityUnhighlightMost = 0.1;
-const OpacityUnhighlightLabel = 0.3;
-
-type contentType = {
-  pfBadge?: PFBadgeType;
-  text: string;
-};
-
-interface GraphStyleOptions {
-  autoungrabify: boolean;
-  autounselectify: boolean;
-  wheelSensitivity: number;
-}
-
-type IHAlign = 'left' | 'center' | 'right';
-type IVAlign = 'top' | 'center' | 'bottom';
-
-interface CytoscapeNodeHtmlParams {
-  cssClass?: string;
-  halign?: IHAlign;
-  halignBox?: IHAlign;
-  query?: string;
-  tpl?: (d: any) => string;
-  valign?: IVAlign;
-  valignBox?: IVAlign;
-}
-
-// Puts a little more space between icons when a badge has multiple icons
-const badgeMargin = (existingIcons: string): string =>
-  existingIcons === '' ? kialiStyle({ marginLeft: '1px' }) : kialiStyle({ marginRight: '2px' });
-
-const badgesDefault = kialiStyle({
-  alignItems: 'center',
-  backgroundColor: NodeBadgeBackgroundColor,
-  borderTopLeftRadius: '3px',
-  borderBottomLeftRadius: '3px',
-  color: NodeBadgeColor,
-  display: 'flex',
-  padding: '3px 3px'
-});
-
-const contentDefault = kialiStyle({
-  alignItems: 'center',
-  backgroundColor: NodeTextBackgroundColor,
-  borderRadius: '3px',
-  borderWidth: '1px',
-  color: NodeTextColor,
-  padding: '1px 5px'
-});
-
-const contentBox = kialiStyle({
-  backgroundColor: NodeTextBackgroundColorBox,
-  color: NodeTextColorBox
-});
-
-const hostsClass = kialiStyle({
-  $nest: {
-    '& div:last-child': {
-      display: 'none'
-    },
-    '&:hover div:last-child': {
-      display: 'block'
-    }
-  }
-});
-
-const hostsList = kialiStyle({
-  textAlign: 'initial',
-  marginTop: 2,
-  paddingTop: 2,
-  borderTop: `1px solid ${PFColors.Color200}`
-});
-
-const labelDefault = kialiStyle({
-  borderRadius: '3px',
-  boxShadow: '0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2px 8px 0 rgba(0, 0, 0, 0.19)',
-  display: 'inline-flex',
-  fontFamily: NodeTextFont,
-  fontWeight: 'normal',
-  marginTop: '4px',
-  textAlign: 'center'
-});
-
-const labelBox = kialiStyle({
-  display: 'block',
-  marginTop: '13px',
-  textAlign: 'left'
-});
-
-export class GraphStyles {
-  private static runtimeColorsSet: boolean;
-
-  // Our node color choices are defined by UX here:
-  // - https://github.com/kiali/kiali/issues/2435#issuecomment-404640317
-  // - https://github.com/kiali/kiali/issues/3675#issuecomment-807403919
-  private static setRuntimeColors = (): void => {
-    if (GraphStyles.runtimeColorsSet) {
-      return;
-    }
-
-    GraphStyles.runtimeColorsSet = true;
-
-    EdgeColor = PFColorVals.Success;
-    EdgeColorDead = PFColorVals.Black500;
-    EdgeColorDegraded = PFColorVals.Warning;
-    EdgeColorFailure = PFColorVals.Danger;
-    EdgeColorTCPWithTraffic = PFColorVals.Blue600;
-    EdgeTextOutlineColor = PFColorVals.White;
-    NodeColorBorder = PFColorVals.Black500;
-    NodeColorBorderBox = PFColorVals.Black600;
-    NodeColorBorderDegraded = PFColorVals.Warning;
-    NodeColorBorderFailure = PFColorVals.Danger;
-    NodeColorBorderHover = PFColorVals.Blue300;
-    NodeColorBorderSelected = PFColorVals.Blue300;
-    NodeColorFill = PFColorVals.White;
-    NodeColorFillBoxApp = PFColorVals.White;
-    NodeColorFillBoxCluster = PFColorVals.Black300;
-    NodeColorFillBoxNamespace = PFColorVals.Black100;
-    NodeColorFillHover = PFColorVals.Blue50;
-    NodeColorFillHoverDegraded = PFColorVals.Orange50;
-    NodeColorFillHoverFailure = PFColorVals.Red50;
-  };
-
-  static options(): GraphStyleOptions {
-    return { wheelSensitivity: 0.1, autounselectify: false, autoungrabify: true };
-  }
-
-  private static getCyGlobalData = (ele: Cy.NodeSingular | Cy.EdgeSingular): CytoscapeGlobalScratchData => {
-    return ele.cy().scratch(CytoscapeGlobalScratchNamespace);
-  };
-
-  static getNodeLabel(ele: Cy.NodeSingular): string {
-    const cyGlobal = GraphStyles.getCyGlobalData(ele);
-    const settings = serverConfig.kialiFeatureFlags.uiDefaults.graph.settings;
-    const zoom = ele.cy().zoom();
-    const noBadge = !cyGlobal.forceLabels && zoom < settings.minFontBadge / settings.fontLabel;
-    const noContent = !cyGlobal.forceLabels && zoom < settings.minFontLabel / settings.fontLabel;
-
-    const node = decoratedNodeData(ele);
-    const app = node.app || '';
-    const cluster = node.cluster;
-    const namespace = node.namespace;
-    const nodeType = node.nodeType;
-    const service = node.service || '';
-    const version = node.version || '';
-    const workload = node.workload || '';
-    const isBox = node.isBox;
-    const isBoxed = node.parent;
-    const box1 = isBoxed ? ele.parent()[0] : undefined;
-    const box1Type = box1 ? box1.data().isBox : undefined;
-    const box2 = box1 && box1.parent() ? box1.parent()[0] : undefined;
-    const box2Type = box2 ? box2.data().isBox : undefined;
-    // const box3 = box2 && box2.parent() ? box2.parent()[0] : undefined;
-    // const box3Type = box3 ? box3.data().isBox : undefined;
-    const isAppBoxed = box1Type === BoxByType.APP;
-    const isNamespaceBoxed = box1Type === BoxByType.NAMESPACE || box2Type === BoxByType.NAMESPACE;
-    // const isClusterBoxed = box1Type === BoxByType.CLUSTER || box2Type === BoxByType.CLUSTER || box3Type === BoxByType.CLUSTER;
-    const isMultiNamespace = cyGlobal.activeNamespaces.length > 1;
-    const isOutside = node.isOutside;
-    const isWaypoint = node.isWaypoint || '';
-
-    // Icon Badges portion of label...
-
-    let badges = '';
-    if (cyGlobal.showOutOfMesh && node.isOutOfMesh) {
-      badges = `<span class="${NodeIconMS} ${badgeMargin(badges)}"></span> ${badges}`;
-    }
-    if (cyGlobal.showVirtualServices) {
-      if (node.hasCB) {
-        badges = `<span class="${NodeIconCB} ${badgeMargin(badges)}"></span> ${badges}`;
-      }
-      // If there's an additional traffic scenario present then it's assumed
-      // that there is a VS present so the VS badge is omitted.
-      if (node.hasVS) {
-        const hasKialiScenario =
-          node.hasFaultInjection ||
-          node.hasMirroring ||
-          node.hasRequestRouting ||
-          node.hasRequestTimeout ||
-          node.hasTCPTrafficShifting ||
-          node.hasTrafficShifting;
-        if (!hasKialiScenario) {
-          badges = `<span class="${NodeIconVS} ${badgeMargin(badges)}"></span> ${badges}`;
-        } else {
-          if (node.hasFaultInjection) {
-            badges = `<span class="${NodeIconFaultInjection} ${badgeMargin(badges)}"></span> ${badges}`;
-          }
-          if (node.hasMirroring) {
-            badges = `<span class="${NodeIconMirroring}  ${badgeMargin(badges)} ${kialiStyle({
-              marginTop: '1px'
-            })}"></span> ${badges}`;
-          }
-          if (node.hasTrafficShifting || node.hasTCPTrafficShifting) {
-            badges = `<span class="${NodeIconTrafficShifting} ${badgeMargin(badges)}"></span> ${badges}`;
-          }
-          if (node.hasRequestTimeout) {
-            badges = `<span class="${NodeIconRequestTimeout} ${badgeMargin(badges)}"></span> ${badges}`;
-          }
-          if (node.hasRequestRouting) {
-            badges = `<span class="${NodeIconRequestRouting} ${badgeMargin(badges)}"></span> ${badges}`;
-          }
-        }
-      }
-
-      if (node.hasWorkloadEntry) {
-        badges = `<span class="${NodeIconWorkloadEntry} ${badgeMargin(badges)}"></span> ${badges}`;
-      }
-
-      if (node.isRoot) {
-        if (
-          node.isGateway?.ingressInfo?.hostnames?.length !== undefined ||
-          node.isGateway?.gatewayAPIInfo?.hostnames?.length !== undefined
-        ) {
-          badges = `<span class="${NodeIconGateway} ${badgeMargin(badges)}"></span> ${badges}`;
-        }
-        badges = `<span class="${NodeIconRoot} ${badgeMargin(badges)}"></span> ${badges}`;
-      } else {
-        if (node.isGateway?.egressInfo?.hostnames?.length !== undefined) {
-          badges = `<span class="${NodeIconGateway} ${badgeMargin(badges)}"></span> ${badges}`;
-        }
-        // A Waypoint should be mutually exclusive with being a traffic source
-        if (isWaypoint) {
-          badges = `<span class="${NodeIconWaypoint} ${badgeMargin(badges)}"></span> ${badges}`;
-        }
-      }
-    }
-
-    const hasBadges = badges.length > 0;
-    const noLabel = noContent && (noBadge || !hasBadges);
-
-    if (hasBadges) {
-      const badgesStyle = noLabel || !noBadge ? '' : 'display:none;';
-      badges = `<div class="${badgesDefault}" style="${badgesStyle}">${badges}</div>`;
-    }
-
-    // Content portion of label (i.e. the text)...
-
-    let contentStyle = '';
-    if (ele.hasClass(HighlightClass)) {
-      const fontSize =
-        isBox && isBox !== BoxByType.APP
-          ? settings.fontLabel * FontSizeRatioHoverBox
-          : settings.fontLabel * FontSizeRatioHover;
-      contentStyle = `font-size:${fontSize}px;`;
-    } else {
-      contentStyle = `font-size:${settings.fontLabel}px;`;
-    }
-    if (!noLabel && noContent) {
-      contentStyle += 'display:none;';
-    }
-
-    const content: contentType[] = [];
-
-    // append namespace if necessary
-    if (
-      (isMultiNamespace || isOutside) &&
-      !!namespace &&
-      namespace !== UNKNOWN &&
-      !isAppBoxed &&
-      !isNamespaceBoxed &&
-      isBox !== BoxByType.NAMESPACE
-    ) {
-      content.push({ pfBadge: PFBadges.Namespace, text: namespace });
-    }
-
-    // append cluster if necessary
-    if (
-      !!cluster &&
-      cluster !== UNKNOWN &&
-      cluster !== cyGlobal.homeCluster &&
-      !isBoxed &&
-      isBox !== BoxByType.CLUSTER
-    ) {
-      content.push({ pfBadge: PFBadges.Cluster, text: cluster });
-    }
-
-    switch (nodeType) {
-      case NodeType.AGGREGATE:
-        content.unshift({ text: node.aggregateValue! });
-        break;
-      case NodeType.APP:
-        if (isAppBoxed) {
-          if (cyGlobal.graphType === GraphType.APP) {
-            content.unshift({ text: app });
-          } else if (version && version !== UNKNOWN) {
-            content.unshift({ text: version });
-          } else {
-            content.unshift({ text: workload ? workload : app });
-          }
-        } else {
-          if (cyGlobal.graphType === GraphType.APP || version === UNKNOWN) {
-            content.unshift({ text: app });
-          } else {
-            content.unshift({ text: version });
-            content.unshift({ text: app });
-          }
-        }
-        break;
-      case NodeType.BOX:
-        switch (isBox) {
-          case BoxByType.APP:
-            content.unshift({ pfBadge: PFBadges.App, text: app });
-            break;
-          case BoxByType.CLUSTER:
-            content.unshift({ pfBadge: PFBadges.Cluster, text: node.cluster });
-            break;
-          case BoxByType.NAMESPACE:
-            content.unshift({ pfBadge: PFBadges.Namespace, text: node.namespace });
-            break;
-        }
-        break;
-      case NodeType.SERVICE:
-        content.unshift({ text: service });
-        break;
-      case NodeType.UNKNOWN:
-        content.unshift({ text: UNKNOWN });
-        break;
-      case NodeType.WORKLOAD:
-        content.unshift({ text: workload });
-        break;
-      default:
-        content.unshift({ text: 'error' });
-    }
-
-    if (node.isExtension) {
-      const extBadge = PFBadges.Extension;
-      extBadge.tt = `${extBadge.tt}: ${node.isExtension.name}`;
-      content[0].pfBadge = extBadge;
-    }
-
-    let contentClasses = `${contentDefault}`;
-
-    // The final label...
-    let fontSize = settings.fontLabel;
-    if (ele.hasClass(HighlightClass)) {
-      fontSize = fontSize * FontSizeRatioHover;
-    }
-    const lineHeight = fontSize + 1;
-    let labelStyle = `font-size:${fontSize}px;line-height:${lineHeight}px;`;
-    if (ele.hasClass(UnhighlightClass)) {
-      labelStyle += `opacity:${OpacityUnhighlightLabel};`;
-    }
-    if (noLabel) {
-      labelStyle += 'display:none;';
-    }
-
-    if (isBox) {
-      let appBoxStyle = isBox === BoxByType.APP ? `font-size: ${settings.fontLabel}px;` : '';
-
-      let contentDivs = '';
-      content.forEach(c => {
-        let contentPfBadge = '';
-        if (!!c.pfBadge) {
-          const pfBadgeStyle = kialiStyle(c.pfBadge.style as NestedCSSProperties);
-          contentPfBadge = `<span class="pf-v5-c-badge pf-m-unread ${kialiBadge} ${pfBadgeStyle}" style="${appBoxStyle}">${c.pfBadge.badge}</span>`;
-        }
-        const contentDiv = `<div class="${contentClasses} ${contentBox}" style="${appBoxStyle} ${contentStyle}">${contentPfBadge}${c.text}</div>`;
-        contentDivs = `${contentDivs}${contentDiv}`;
-      });
-      return `<div class="${labelDefault} ${labelBox}" style="${labelStyle}">${badges}${contentDivs}</div>`;
-    }
-
-    let hosts: string[] = [];
-    node.hasVS?.hostnames?.forEach(h => hosts.push(h === '*' ? '* (all hosts)' : h));
-    node.isGateway?.ingressInfo?.hostnames?.forEach(h => hosts.push(h === '*' ? '* (all hosts)' : h));
-    node.isGateway?.egressInfo?.hostnames?.forEach(h => hosts.push(h === '*' ? '* (all hosts)' : h));
-    node.isGateway?.gatewayAPIInfo?.hostnames?.forEach(h => hosts.push(h === '*' ? '* (all hosts)' : h));
-
-    let htmlHosts = '';
-    if (hosts.length !== 0) {
-      let hostsToShow = hosts;
-      if (hostsToShow.length > config.graph.maxHosts) {
-        hostsToShow = hosts.slice(0, config.graph.maxHosts);
-        hostsToShow.push(
-          hosts.length - config.graph.maxHosts === 1
-            ? '1 more host...'
-            : `${hosts.length - config.graph.maxHosts} more hosts...`
-        );
-      }
-      htmlHosts = `<div class="${hostsList}">${hostsToShow.join('<br />')}</div>`;
-    }
-
-    if (hosts.length > 0) {
-      contentClasses = `${contentClasses} ${hostsClass}`;
-    }
-
-    let contentDivs = '';
-    content.forEach(c => {
-      let contentPfBadge = '';
-      if (!!c.pfBadge) {
-        const pfBadgeStyle = kialiStyle(c.pfBadge.style as NestedCSSProperties);
-        contentPfBadge = `<span class="pf-v5-c-badge pf-m-unread ${kialiBadge} ${pfBadgeStyle}" style="${''}">${
-          c.pfBadge.badge
-        }</span>`;
-      }
-      const contentDiv = `<div class="${contentClasses}" style="${''} ${contentStyle}">${contentPfBadge}${
-        c.text
-      }</div>`;
-      contentDivs = `${contentDivs}${contentDiv}`;
-    });
-
-    const contentSpan = `<div class="${contentClasses}" style="${contentStyle}">${contentDivs}${htmlHosts}</div>`;
-    return `<div class="${labelDefault}" style="${labelStyle}">${badges}${contentSpan}</div>`;
-  }
-
-  static htmlNodeLabels(cy: Cy.Core): CytoscapeNodeHtmlParams[] {
-    return [
-      {
-        query: 'node[^isBox]:visible', // leaf nodes
-        halign: 'center',
-        valign: 'bottom',
-        halignBox: 'center',
-        valignBox: 'bottom',
-        tpl: (data: any) => this.getNodeLabel(cy.$id(data.id))
-      },
-      {
-        query: 'node[isBox = "app"]:visible', // app box nodes
-        halign: 'center',
-        valign: 'bottom',
-        halignBox: 'center',
-        valignBox: 'bottom',
-        tpl: (data: any) => this.getNodeLabel(cy.$id(data.id))
-      },
-      {
-        query: 'node[isBox = "namespace"]:visible', // ns box nodes
-        halign: 'center',
-        valign: 'bottom',
-        halignBox: 'center',
-        valignBox: 'bottom',
-        tpl: (data: any) => this.getNodeLabel(cy.$id(data.id))
-      },
-      {
-        query: 'node[isBox = "cluster"]:visible', // cluster box nodes
-        halign: 'center',
-        valign: 'bottom',
-        halignBox: 'center',
-        valignBox: 'bottom',
-        tpl: (data: any) => this.getNodeLabel(cy.$id(data.id))
-      }
-    ];
-  }
-
-  static getEdgeLabel = (ele: Cy.EdgeSingular, isVerbose?: boolean): string => {
-    const settings = serverConfig.kialiFeatureFlags.uiDefaults.graph.settings;
-    const zoom = ele.cy().zoom();
-    const cyGlobal = GraphStyles.getCyGlobalData(ele);
-    const noLabel = !cyGlobal.forceLabels && zoom < settings.minFontLabel / settings.fontLabel;
-
-    if (noLabel) {
-      return '';
-    }
-
-    const edgeLabels = cyGlobal.edgeLabels;
-    const edgeData = decoratedEdgeData(ele);
-    const includeUnits = isVerbose || numLabels(edgeLabels) > 1;
-    let labels = [] as string[];
-
-    if (edgeLabels.includes(EdgeLabelMode.TRAFFIC_RATE)) {
-      let rate = 0;
-      let pErr = 0;
-      if (edgeData.http > 0) {
-        rate = edgeData.http;
-        pErr = edgeData.httpPercentErr > 0 ? edgeData.httpPercentErr : 0;
-      } else if (edgeData.grpc > 0) {
-        rate = edgeData.grpc;
-        pErr = edgeData.grpcPercentErr > 0 ? edgeData.grpcPercentErr : 0;
-      } else if (edgeData.tcp > 0) {
-        rate = edgeData.tcp;
-      }
-
-      if (rate > 0) {
-        if (pErr > 0) {
-          labels.push(`${GraphStyles.toFixedRequestRate(rate, includeUnits)}\n${GraphStyles.toFixedErrRate(pErr)}`);
-        } else {
-          switch (edgeData.protocol) {
-            case Protocol.GRPC:
-              if (cyGlobal.trafficRates.includes(TrafficRate.GRPC_REQUEST)) {
-                labels.push(GraphStyles.toFixedRequestRate(rate, includeUnits));
-              } else {
-                labels.push(GraphStyles.toFixedRequestRate(rate, includeUnits, 'mps'));
-              }
-              break;
-            case Protocol.TCP:
-              labels.push(GraphStyles.toFixedByteRate(rate, includeUnits));
-              break;
-            default:
-              labels.push(GraphStyles.toFixedRequestRate(rate, includeUnits));
-              break;
-          }
-        }
-      }
-    }
-
-    if (edgeLabels.includes(EdgeLabelMode.RESPONSE_TIME_GROUP)) {
-      let responseTime = edgeData.responseTime;
-
-      if (responseTime > 0) {
-        labels.push(GraphStyles.toFixedDuration(responseTime));
-      }
-    }
-
-    if (edgeLabels.includes(EdgeLabelMode.THROUGHPUT_GROUP)) {
-      let rate = edgeData.throughput;
-
-      if (rate > 0) {
-        labels.push(GraphStyles.toFixedByteRate(rate, includeUnits));
-      }
-    }
-
-    if (edgeLabels.includes(EdgeLabelMode.TRAFFIC_DISTRIBUTION)) {
-      let pReq;
-      if (edgeData.httpPercentReq > 0) {
-        pReq = edgeData.httpPercentReq;
-      } else if (edgeData.grpcPercentReq > 0) {
-        pReq = edgeData.grpcPercentReq;
-      }
-      if (pReq > 0 && pReq < 100) {
-        labels.push(GraphStyles.toFixedPercent(pReq));
-      }
-    }
-
-    let label = labels.join('\n');
-
-    if (isVerbose) {
-      const protocol = edgeData.protocol;
-      label = protocol ? `${protocol}\n${label}` : label;
-    }
-
-    const mtlsPercentage = edgeData.isMTLS;
-    let lockIcon = false;
-    if (cyGlobal.showSecurity && edgeData.hasTraffic) {
-      if (mtlsPercentage && mtlsPercentage > 0) {
-        lockIcon = true;
-        label = `${EdgeIconMTLS}\n${label}`;
-      }
-    }
-
-    if (edgeData.hasTraffic && edgeData.responses) {
-      const dest = decoratedNodeData(ele.target());
-      if (dest.hasCB) {
-        const responses = edgeData.responses;
-        for (let code of _.keys(responses)) {
-          // TODO: Not 100% sure we want "UH" code here ("no healthy upstream hosts") but based on timing I have
-          // seen this code returned and not "UO". "UO" is returned only when the circuit breaker is caught open.
-          // But if open CB is responsible for removing possible destinations the "UH" code seems preferred.
-          if (responses[code]['UO'] || responses[code]['UH']) {
-            label = lockIcon ? `${NodeIconCB} ${label}` : `${NodeIconCB}\n${label}`;
-            break;
-          }
-        }
-      }
-    }
-
-    return label;
-  };
-
-  private static trimFixed = (fixed: string): string => {
-    if (!fixed.includes('.')) {
-      return fixed;
-    }
-    while (fixed.endsWith('0')) {
-      fixed = fixed.slice(0, -1);
-    }
-    return fixed.endsWith('.') ? (fixed = fixed.slice(0, -1)) : fixed;
-  };
-
-  private static toFixedRequestRate = (num: number, includeUnits: boolean, units?: string): string => {
-    num = GraphStyles.safeNum(num);
-    const rate = GraphStyles.trimFixed(num.toFixed(2));
-    return includeUnits ? `${rate} ${units || 'rps'}` : rate;
-  };
-
-  private static toFixedErrRate = (num: number): string => {
-    num = GraphStyles.safeNum(num);
-    return `${GraphStyles.trimFixed(num.toFixed(num < 1 ? 1 : 0))}% err`;
-  };
-
-  private static toFixedByteRate = (num: number, includeUnits: boolean): string => {
-    num = GraphStyles.safeNum(num);
-    if (num < 1024.0) {
-      const rate = num < 1.0 ? GraphStyles.trimFixed(num.toFixed(2)) : num.toFixed(0);
-      return includeUnits ? `${rate} bps` : rate;
-    }
-    const rate = GraphStyles.trimFixed((num / 1024.0).toFixed(2));
-    return includeUnits ? `${rate} kps` : rate;
-  };
-
-  private static toFixedPercent = (num: number): string => {
-    num = GraphStyles.safeNum(num);
-    return `${GraphStyles.trimFixed(num.toFixed(1))}%`;
-  };
-
-  private static toFixedDuration = (num: number): string => {
-    num = GraphStyles.safeNum(num);
-    if (num < 1000) {
-      return `${num.toFixed(0)}ms`;
-    }
-    return `${GraphStyles.trimFixed((num / 1000.0).toFixed(2))}s`;
-  };
-
-  // This is due to us never having figured out why a tiny fraction of what-we-expect-to-be-numbers
-  // are in fact strings.  We don't know if our conversion in GraphData.ts has a flaw, or whether
-  // something else happens post-conversion.
-  private static safeNum = (num: any): number => {
-    if (Number.isFinite(num)) {
-      return num;
-    }
-    if (typeof num === 'string' || num instanceof String) {
-      console.warn(`GraphStyles: Expected number but received string: |${num}|`);
-    }
-    // this will return NaN if the string is 'NaN' or any other non-number
-    return Number(num);
-  };
-
-  static styles(): Cy.Stylesheet[] {
-    GraphStyles.setRuntimeColors();
-
-    const getEdgeColor = (ele: Cy.EdgeSingular): string => {
-      const edgeData = decoratedEdgeData(ele);
-
-      if (!edgeData.hasTraffic) {
-        return EdgeColorDead;
-      }
-      if (edgeData.protocol === 'tcp') {
-        return EdgeColorTCPWithTraffic;
-      }
-
-      switch (edgeData.healthStatus) {
-        case FAILURE.name:
-          return EdgeColorFailure;
-        case DEGRADED.name:
-          return EdgeColorDegraded;
-        default:
-          return EdgeColor;
-      }
-    };
-
-    const getNodeBackgroundImage = (ele: Cy.NodeSingular): string => {
-      const nodeData = decoratedNodeData(ele);
-      const isInaccessible = nodeData.isInaccessible;
-      const isServiceEntry = nodeData.isServiceEntry;
-      const isBox = nodeData.isBox;
-      if (isInaccessible && !isServiceEntry && !isBox) {
-        return NodeImageKey;
-      }
-      const isOutside = nodeData.isOutside;
-      if (isOutside && !isBox) {
-        return NodeImageTopology;
-      }
-      return 'none';
-    };
-
-    const getNodeBackgroundPositionX = (ele: Cy.NodeSingular): string => {
-      if (getNodeShape(ele) === 'round-tag') {
-        return '0';
-      }
-      return '50%';
-    };
-
-    const getNodeBackgroundPositionY = (ele: Cy.NodeSingular): string => {
-      if (getNodeShape(ele) === 'round-triangle') {
-        return '6px';
-      }
-      return '50%';
-    };
-
-    const getNodeBorderColor = (ele: Cy.NodeSingular): string => {
-      const isBox = ele.data(NodeAttr.isBox);
-      if (isBox && isBox !== BoxByType.APP) {
-        return NodeColorBorderBox;
-      }
-
-      const healthStatus = ele.data(NodeAttr.healthStatus);
-      switch (healthStatus) {
-        case DEGRADED.name:
-          return NodeColorBorderDegraded;
-        case FAILURE.name:
-          return NodeColorBorderFailure;
-        default:
-          return NodeColorBorder;
-      }
-    };
-
-    const getNodeShape = (ele: Cy.NodeSingular): Cy.Css.NodeShape => {
-      const nodeData = decoratedNodeData(ele);
-      const nodeType = nodeData.nodeType;
-      switch (nodeType) {
-        case NodeType.AGGREGATE:
-          return 'round-pentagon';
-        case NodeType.APP:
-          return 'round-rectangle';
-        case NodeType.BOX:
-          return 'round-rectangle';
-        case NodeType.SERVICE:
-          return nodeData.isServiceEntry ? 'round-tag' : 'round-triangle';
-        case NodeType.UNKNOWN:
-          return 'ellipse';
-        case NodeType.WORKLOAD:
-          return 'ellipse';
-        default:
-          return 'ellipse';
-      }
-    };
-
-    const nodeSelectedStyle = {
-      'border-color': (ele: Cy.NodeSingular) => {
-        switch (ele.data(NodeAttr.healthStatus)) {
-          case DEGRADED.name:
-            return NodeColorBorderDegraded;
-          case FAILURE.name:
-            return NodeColorBorderFailure;
-          default:
-            return NodeColorBorderSelected;
-        }
-      },
-      'border-width': NodeBorderWidthSelected
-    };
-
-    return [
-      // Node Defaults
-      {
-        selector: 'node',
-        css: {
-          'background-color': NodeColorFill,
-          'background-image': (ele: Cy.NodeSingular) => {
-            return getNodeBackgroundImage(ele);
-          },
-          'background-width': '80%',
-          'background-height': '80%',
-          'background-position-x': getNodeBackgroundPositionX,
-          'background-position-y': getNodeBackgroundPositionY,
-          'border-color': (ele: Cy.NodeSingular) => {
-            return getNodeBorderColor(ele);
-          },
-          'border-style': (ele: Cy.NodeSingular) => {
-            return decoratedNodeData(ele).isIdle ? 'dotted' : 'solid';
-          },
-          'border-width': NodeBorderWidth,
-          ghost: 'yes',
-          'ghost-offset-x': 1,
-          'ghost-offset-y': 1,
-          'ghost-opacity': 0.4,
-          height: NodeHeight,
-          shape: (ele: Cy.NodeSingular) => {
-            return getNodeShape(ele);
-          },
-          width: NodeWidth,
-          'z-index': 10
-        }
-      },
-      // Node is a Cluster Box
-      {
-        selector: `node[isBox="${BoxByType.CLUSTER}"]`,
-        css: {
-          'background-color': NodeColorFillBoxCluster
-        }
-      },
-      // Node is a Namespace Box
-      {
-        selector: `node[isBox="${BoxByType.NAMESPACE}"]`,
-        css: {
-          'background-color': NodeColorFillBoxNamespace
-        }
-      },
-      // Node is an App Box
-      {
-        selector: `node[isBox="${BoxByType.APP}"]`,
-        css: {
-          'background-color': NodeColorFillBoxApp
-        }
-      },
-      // Node is selected
-      {
-        selector: 'node:selected',
-        style: nodeSelectedStyle
-      },
-      // Node other than Box is highlighted (see GraphHighlighter.ts)
-      {
-        selector: `node.${HighlightClass}[^isBox]`,
-        style: {
-          'background-color': (ele: Cy.NodeSingular) => {
-            switch (ele.data(NodeAttr.healthStatus)) {
-              case DEGRADED.name:
-                return NodeColorFillHoverDegraded;
-              case FAILURE.name:
-                return NodeColorFillHoverFailure;
-              default:
-                return NodeColorFillHover;
-            }
-          },
-          'border-color': (ele: Cy.NodeSingular) => {
-            switch (ele.data(NodeAttr.healthStatus)) {
-              case DEGRADED.name:
-                return NodeColorBorderDegraded;
-              case FAILURE.name:
-                return NodeColorBorderFailure;
-              default:
-                return NodeColorBorderHover;
-            }
-          }
-        }
-      },
-      // Node unhighlighting (see GraphHighlighter.ts)
-      // 1. for regular, unboxed nodes, use min opacity
-      {
-        selector: `node.${UnhighlightClass}[^isBox][^parent]`,
-        style: {
-          opacity: OpacityUnhighlightMost
-        }
-      },
-      // 2. for regular, boxed nodes, use max opacity because the box's opacity will compound the effect
-      {
-        selector: `node.${UnhighlightClass}[^isBox][parent]`,
-        style: {
-          opacity: OpacityUnhighlightLeast
-        }
-      },
-      // 3. for boxes start with medium opacity
-      {
-        selector: `node.${UnhighlightClass}[isBox]`,
-        style: {
-          opacity: OpacityUnhighlightMedium
-        }
-      },
-      // 4. but reset inner boxes or the compounded opacity is too much
-      {
-        selector: `node.${UnhighlightClass}[isBox][^parent] node[isBox]`,
-        style: {
-          opacity: 1.0
-        }
-      },
-      {
-        selector: 'edge',
-        css: {
-          'curve-style': 'bezier',
-          'font-family': EdgeTextFont,
-          'font-size': `${
-            serverConfig.kialiFeatureFlags.uiDefaults.graph.settings.fontLabel * FontSizeRatioEdgeText
-          }px`,
-          label: (ele: Cy.EdgeSingular) => {
-            return GraphStyles.getEdgeLabel(ele);
-          },
-          'line-color': (ele: Cy.EdgeSingular) => {
-            return getEdgeColor(ele);
-          },
-          'line-style': 'solid',
-          'source-arrow-color': (ele: Cy.EdgeSingular) => {
-            return getEdgeColor(ele);
-          },
-          'target-arrow-shape': 'vee',
-          'target-arrow-color': (ele: Cy.EdgeSingular) => {
-            return getEdgeColor(ele);
-          },
-          'text-events': 'yes',
-          'text-outline-color': EdgeTextOutlineColor,
-          'text-outline-width': EdgeTextOutlineWidth,
-          'text-wrap': 'wrap',
-          width: EdgeWidth
-        }
-      },
-      {
-        selector: 'edge:selected',
-        css: {
-          width: EdgeWidthSelected,
-          label: (ele: Cy.EdgeSingular) => GraphStyles.getEdgeLabel(ele, true)
-        }
-      },
-      {
-        selector: 'edge[protocol="tcp"]',
-        css: {
-          'target-arrow-shape': 'triangle-cross',
-          'line-style': 'solid'
-        }
-      },
-      {
-        selector: `edge.${HighlightClass}`,
-        style: {
-          'font-size': `${serverConfig.kialiFeatureFlags.uiDefaults.graph.settings.fontLabel}px`
-        }
-      },
-      {
-        selector: `edge.${HoveredClass}`,
-        style: {
-          label: (ele: Cy.EdgeSingular) => {
-            return GraphStyles.getEdgeLabel(ele);
-          }
-        }
-      },
-      {
-        selector: `edge.${UnhighlightClass}`,
-        style: {
-          opacity: OpacityUnhighlightMost
-        }
-      },
-      {
-        selector: '*.find[^isBox]',
-        style: {
-          'overlay-color': PFColorVals.Gold400,
-          'overlay-padding': '7px',
-          'overlay-opacity': OpacityOverlay
-        }
-      },
-      {
-        selector: '*.span[^isBox]',
-        style: {
-          'overlay-color': PFColorVals.Purple200,
-          'overlay-padding': '7px',
-          'overlay-opacity': OpacityOverlay
-        }
-      },
-      {
-        selector: `edge[waypoint]`,
-        style: {
-          'source-arrow-shape': (ele: Cy.EdgeSingular) => {
-            if (ele.data().waypoint.direction === 'to' && ele.data().waypoint.fromEdge) {
-              return 'triangle-cross';
-            }
-            return 'none';
-          }
-        }
-      }
-    ];
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/graphs/KialiBreadthFirstGraph.ts b/frontend/src/components/CytoscapeGraph/graphs/KialiBreadthFirstGraph.ts
deleted file mode 100644
index 85a827914..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/KialiBreadthFirstGraph.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export class KialiBreadthFirstGraph {
-  static getLayout() {
-    return {
-      name: 'kiali-breadthfirst',
-      animate: false,
-      fit: false,
-      nodeDimensionsIncludeLabels: true,
-      padding: 30
-    };
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/graphs/KialiConcentricGraph.ts b/frontend/src/components/CytoscapeGraph/graphs/KialiConcentricGraph.ts
deleted file mode 100644
index b044908ca..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/KialiConcentricGraph.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export class KialiConcentricGraph {
-  static getLayout() {
-    return {
-      name: 'kiali-concentric',
-      animate: false,
-      fit: false,
-      nodeDimensionsIncludeLabels: true
-    };
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/graphs/KialiDagreGraph.ts b/frontend/src/components/CytoscapeGraph/graphs/KialiDagreGraph.ts
deleted file mode 100644
index 4f7ed078c..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/KialiDagreGraph.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export class KialiDagreGraph {
-  static getLayout() {
-    return {
-      name: 'kiali-dagre',
-      fit: false,
-      nodeDimensionsIncludeLabels: true,
-      rankDir: 'LR'
-    };
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/graphs/KialiGridGraph.ts b/frontend/src/components/CytoscapeGraph/graphs/KialiGridGraph.ts
deleted file mode 100644
index 7c4fb5d85..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/KialiGridGraph.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export class KialiGridGraph {
-  static getLayout() {
-    return {
-      name: 'kiali-grid',
-      animate: false,
-      fit: false,
-      nodeDimensionsIncludeLabels: true,
-      padding: 0,
-      condense: false // Condense false works well with NO BoxLayout, but not optimized for namespaces layout
-    };
-  }
-}
diff --git a/frontend/src/components/CytoscapeGraph/graphs/LayoutDictionary.ts b/frontend/src/components/CytoscapeGraph/graphs/LayoutDictionary.ts
deleted file mode 100644
index bbe403f5a..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/LayoutDictionary.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Layout } from '../../../types/Graph';
-import { KialiDagreGraph } from './KialiDagreGraph';
-import { KialiGridGraph } from './KialiGridGraph';
-import { KialiConcentricGraph } from './KialiConcentricGraph';
-import { KialiBreadthFirstGraph } from './KialiBreadthFirstGraph';
-
-const LayoutMap = {
-  'kiali-breadthfirst': KialiBreadthFirstGraph.getLayout(),
-  'kiali-dagre': KialiDagreGraph.getLayout(),
-  'kiali-grid': KialiGridGraph.getLayout(),
-  'kiali-concentric': KialiConcentricGraph.getLayout()
-};
-
-const getLayout = (layout: Layout) =>
-  LayoutMap.hasOwnProperty(layout.name) ? LayoutMap[layout.name] : LayoutMap['kiali-dagre'];
-
-const getLayoutByName = (layoutName: string) =>
-  LayoutMap.hasOwnProperty(layoutName) ? LayoutMap[layoutName] : LayoutMap['kiali-dagre'];
-
-export { getLayout, getLayoutByName };
diff --git a/frontend/src/components/CytoscapeGraph/graphs/__tests__/GraphStyles.test.ts b/frontend/src/components/CytoscapeGraph/graphs/__tests__/GraphStyles.test.ts
deleted file mode 100644
index 420af98c3..000000000
--- a/frontend/src/components/CytoscapeGraph/graphs/__tests__/GraphStyles.test.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import cytoscape from 'cytoscape';
-
-import { GraphStyles } from '../GraphStyles';
-import { CytoscapeGlobalScratchNamespace, NodeType } from '../../../../types/Graph';
-import { icons } from '../../../../config';
-
-const nodeData = {
-  id: 'n2',
-  text: 'details (v1)',
-  service: 'details.istio-system.svc.cluster.local',
-  version: 'v1',
-  cluster: `cluster-1`,
-  namespace: 'istio-system',
-  nodeType: NodeType.APP
-};
-
-function setupNode(nodeData: any, cyData?: any) {
-  const data = cytoscape({
-    elements: {
-      nodes: [{ data: nodeData }],
-      edges: []
-    }
-  });
-  const node = data.nodes()[0];
-  node.cy().scratch(CytoscapeGlobalScratchNamespace, {
-    activeNamespaces: ['TEST'],
-    showVirtualServices: true,
-    ...cyData
-  });
-  return node;
-}
-
-describe('GraphStyles test', () => {
-  it('has icon for vs', () => {
-    const data = { ...nodeData, hasVS: {} };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.virtualService.className);
-  });
-
-  it('has icon for circuit breaker', () => {
-    const data = { ...nodeData, hasVS: {}, hasCB: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.circuitBreaker.className);
-  });
-
-  it('has icon for circuit breaker even when no VS', () => {
-    const data = { ...nodeData, hasCB: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.circuitBreaker.className);
-  });
-
-  it('hides circuit breaker icon when showVirtualServices is false', () => {
-    const data = { ...nodeData, hasVS: {}, hasCB: true };
-    const node = setupNode(data, { showVirtualServices: false });
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label.includes(icons.istio.circuitBreaker.className)).toBe(false);
-  });
-
-  it('has icon for fault injection', () => {
-    const data = { ...nodeData, hasVS: {}, hasFaultInjection: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.faultInjection.className);
-  });
-
-  it('has icon for mirroring', () => {
-    const data = { ...nodeData, hasVS: {}, hasMirroring: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.mirroring.className);
-  });
-
-  it('has icon for request routing', () => {
-    const data = { ...nodeData, hasVS: {}, hasRequestRouting: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.requestRouting.className);
-  });
-
-  it('has icon for request routing', () => {
-    const data = { ...nodeData, hasVS: {}, hasRequestRouting: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.requestRouting.className);
-  });
-
-  it('has icon for request timeout', () => {
-    const data = { ...nodeData, hasVS: {}, hasRequestTimeout: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.requestTimeout.className);
-  });
-
-  it('has icon for traffic shifting', () => {
-    const data = { ...nodeData, hasVS: {}, hasTrafficShifting: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.trafficShifting.className);
-  });
-
-  it('has icon for tcp traffic shifting', () => {
-    const data = { ...nodeData, hasVS: {}, hasTCPTrafficShifting: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-    expect(label).toContain(icons.istio.trafficShifting.className);
-  });
-
-  it('always displays root as leftmost icon', () => {
-    const data = { ...nodeData, hasVS: {}, hasTCPTrafficShifting: true, isRoot: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-
-    const firstSpanBegin = label.search('<span');
-    const firstSpanEnd = label.search('</span>');
-    expect(label.substring(firstSpanBegin, firstSpanEnd)).toContain(icons.istio.root.className);
-  });
-
-  it('displays request routing icon before other kiali scenarios', () => {
-    const data = { ...nodeData, hasVS: true, hasTCPTrafficShifting: true, hasRequestRouting: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-
-    const firstSpanBegin = label.search('<span');
-    const firstSpanEnd = label.search('</span>');
-    expect(label.substring(firstSpanBegin, firstSpanEnd)).toContain(icons.istio.requestRouting.className);
-  });
-
-  it('displays vm icon when workload entry present', () => {
-    const data = { ...nodeData, hasWorkloadEntry: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-
-    expect(label).toContain(icons.istio.workloadEntry.className);
-  });
-
-  it('displays root as leftmost when workload entry present', () => {
-    const data = { ...nodeData, hasWorkloadEntry: true, isRoot: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-
-    const firstSpanBegin = label.search('<span');
-    const firstSpanEnd = label.search('</span>');
-    expect(label.substring(firstSpanBegin, firstSpanEnd)).toContain(icons.istio.root.className);
-  });
-
-  it('displays workload entry as leftmost when not root', () => {
-    const data = { ...nodeData, hasWorkloadEntry: true, hasMissingSC: true };
-    const node = setupNode(data);
-    const label = GraphStyles.getNodeLabel(node);
-
-    const firstSpanBegin = label.search('<span');
-    const firstSpanEnd = label.search('</span>');
-    expect(label.substring(firstSpanBegin, firstSpanEnd)).toContain(icons.istio.workloadEntry.className);
-  });
-});
diff --git a/frontend/src/components/MTls/MTLSIcon.tsx b/frontend/src/components/MTls/MTLSIcon.tsx
index a5b17ceb7..90e5388aa 100644
--- a/frontend/src/components/MTls/MTLSIcon.tsx
+++ b/frontend/src/components/MTls/MTLSIcon.tsx
@@ -6,6 +6,7 @@ import fullIconDark from '../../assets/img/mtls/mtls-status-full-dark.svg';
 import hollowIconDark from '../../assets/img/mtls/mtls-status-partial-dark.svg';
 import { useKialiTheme } from 'utils/ThemeUtils';
 import { Theme } from 'types/Common';
+import { MTLSIconTypes } from './NamespaceMTLSStatus';
 
 type MTLSIconProps = {
   icon: string;
@@ -14,11 +15,6 @@ type MTLSIconProps = {
   tooltipText: string;
 };
 
-export enum MTLSIconTypes {
-  LOCK_FULL = 'LOCK_FULL',
-  LOCK_HOLLOW = 'LOCK_HOLLOW'
-}
-
 export const MTLSIcon: React.FC<MTLSIconProps> = (props: MTLSIconProps) => {
   const darkTheme = useKialiTheme() === Theme.DARK;
   const [mtlsIcon, setMtlsIcon] = React.useState('');
diff --git a/frontend/src/components/MTls/MeshMTLSStatus.tsx b/frontend/src/components/MTls/MeshMTLSStatus.tsx
index 389ee82d7..dcd76547d 100644
--- a/frontend/src/components/MTls/MeshMTLSStatus.tsx
+++ b/frontend/src/components/MTls/MeshMTLSStatus.tsx
@@ -1,7 +1,6 @@
 import * as React from 'react';
 
 import { KialiAppState } from '../../store/Store';
-import { MTLSIconTypes } from './MTLSIcon';
 import { MTLSStatus, emptyDescriptor, StatusDescriptor } from './MTLSStatus';
 import { kialiStyle } from 'styles/StyleUtils';
 import { meshWideMTLSEnabledSelector, meshWideMTLSStatusSelector, namespaceItemsSelector } from '../../store/Selectors';
@@ -16,6 +15,7 @@ import { MeshTlsActions } from '../../actions/MeshTlsActions';
 import { TimeInMilliseconds } from '../../types/Common';
 import { Namespace } from '../../types/Namespace';
 import { connectRefresh } from '../Refresh/connectRefresh';
+import { MTLSIconTypes } from './NamespaceMTLSStatus';
 
 type ReduxProps = {
   autoMTLSEnabled: boolean;
diff --git a/frontend/src/components/MTls/NamespaceMTLSStatus.tsx b/frontend/src/components/MTls/NamespaceMTLSStatus.tsx
index 26fd0776c..a845bd1b4 100644
--- a/frontend/src/components/MTls/NamespaceMTLSStatus.tsx
+++ b/frontend/src/components/MTls/NamespaceMTLSStatus.tsx
@@ -1,10 +1,13 @@
 import * as React from 'react';
-
-import { MTLSIconTypes } from './MTLSIcon';
 import { MTLSStatus, emptyDescriptor, StatusDescriptor } from './MTLSStatus';
 import { kialiStyle } from 'styles/StyleUtils';
 import { MTLSStatuses } from '../../types/TLSStatus';
 
+export enum MTLSIconTypes {
+  LOCK_FULL = 'LOCK_FULL',
+  LOCK_HOLLOW = 'LOCK_HOLLOW'
+}
+
 type Props = {
   status: string;
 };
diff --git a/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx b/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx
index a7d91092b..1337f9139 100644
--- a/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx
+++ b/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx
@@ -1,10 +1,11 @@
 import * as React from 'react';
+import { MTLSIcon } from '../MTLSIcon';
 import { mount } from 'enzyme';
-import { MTLSIcon, MTLSIconTypes } from '../MTLSIcon';
 import { mountToJson } from 'enzyme-to-json';
 import { Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { Provider } from 'react-redux';
 import { store } from 'store/ConfigStore';
+import { MTLSIconTypes } from '../NamespaceMTLSStatus';
 
 const mockIcon = (icon: string) => {
   const component = (
diff --git a/frontend/src/components/Nav/Menu.tsx b/frontend/src/components/Nav/Menu.tsx
index aee9fcca7..e4610f007 100644
--- a/frontend/src/components/Nav/Menu.tsx
+++ b/frontend/src/components/Nav/Menu.tsx
@@ -3,7 +3,6 @@ import * as React from 'react';
 import { Link, useLocation, useNavigate, matchPath } from 'react-router-dom-v5-compat';
 import { Nav, NavList, NavItem } from '@patternfly/react-core';
 import { navMenuItems } from '../../routes';
-import { serverConfig } from '../../config';
 import { kialiStyle } from 'styles/StyleUtils';
 import { ExternalServiceInfo } from '../../types/StatusState';
 import { KialiIcon } from 'config/KialiIcon';
@@ -60,8 +59,6 @@ export const Menu: React.FC<MenuProps> = (props: MenuProps) => {
 
   const renderMenuItems = (): React.ReactNode => {
     const allNavMenuItems = navMenuItems;
-    const graphEnableCytoscape = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'pf';
-    const graphEnablePatternfly = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'cy';
 
     const activeMenuItem = allNavMenuItems.find(item => {
       let isRoute = matchPath({ path: item.to }, pathname) ? true : false;
@@ -77,14 +74,6 @@ export const Menu: React.FC<MenuProps> = (props: MenuProps) => {
 
     return allNavMenuItems
       .filter(item => {
-        if (item.id === 'traffic_graph_cy') {
-          return graphEnableCytoscape;
-        }
-
-        if (item.id === 'traffic_graph_pf') {
-          return graphEnablePatternfly;
-        }
-
         if (item.id === 'mesh') {
           return isControlPlaneAccessible();
         }
@@ -98,13 +87,6 @@ export const Menu: React.FC<MenuProps> = (props: MenuProps) => {
           return tracingUrl && <ExternalLink key={item.to} href={tracingUrl} name={t(title)} />;
         }
 
-        if (
-          (item.id === 'traffic_graph_cy' && !graphEnablePatternfly) ||
-          (item.id === 'traffic_graph_pf' && !graphEnableCytoscape)
-        ) {
-          title = t('Traffic Graph');
-        }
-
         return (
           <NavItem isActive={activeMenuItem === item} key={item.to}>
             <Link id={item.id} to={item.to} onClick={() => navigate(item.to)}>
diff --git a/frontend/src/components/Nav/NavUtils.tsx b/frontend/src/components/Nav/NavUtils.tsx
index f0115509e..656ac5cea 100644
--- a/frontend/src/components/Nav/NavUtils.tsx
+++ b/frontend/src/components/Nav/NavUtils.tsx
@@ -1,18 +1,19 @@
-import { Layout, EdgeLabelMode, NodeType, NodeParamsType, GraphType, TrafficRate, EdgeMode } from '../../types/Graph';
+import { EdgeLabelMode, NodeType, NodeParamsType, GraphType, TrafficRate, EdgeMode } from '../../types/Graph';
 import { DurationInSeconds, IntervalInMilliseconds } from '../../types/Common';
 import { Namespace } from '../../types/Namespace';
 import { URLParam } from '../../app/History';
 import { getKioskMode, isKioskMode } from '../../utils/SearchParamUtils';
 import { isMultiCluster } from 'config';
+import { GraphLayout } from 'pages/Graph/GraphPF';
 
 export type GraphUrlParams = {
   activeNamespaces: Namespace[];
   duration: DurationInSeconds;
   edgeLabels: EdgeLabelMode[];
   edgeMode: EdgeMode;
-  graphLayout: Layout;
+  graphLayout: GraphLayout;
   graphType: GraphType;
-  namespaceLayout: Layout;
+  namespaceLayout: GraphLayout;
   node?: NodeParamsType;
   refreshInterval: IntervalInMilliseconds;
   showIdleEdges: boolean;
@@ -26,8 +27,8 @@ export type GraphUrlParams = {
 const buildCommonQueryParams = (params: GraphUrlParams): string => {
   let q = `&${URLParam.GRAPH_EDGE_LABEL}=${params.edgeLabels}`;
   q += `&${URLParam.GRAPH_EDGE_MODE}=${params.edgeMode}`;
-  q += `&${URLParam.GRAPH_LAYOUT}=${params.graphLayout.name}`;
-  q += `&${URLParam.GRAPH_NAMESPACE_LAYOUT}=${params.namespaceLayout.name}`;
+  q += `&${URLParam.GRAPH_LAYOUT}=${params.graphLayout}`;
+  q += `&${URLParam.GRAPH_NAMESPACE_LAYOUT}=${params.namespaceLayout}`;
   q += `&${URLParam.GRAPH_IDLE_EDGES}=${params.showIdleEdges}`;
   q += `&${URLParam.GRAPH_IDLE_NODES}=${params.showIdleNodes}`;
   q += `&${URLParam.GRAPH_SERVICE_NODES}=${params.showServiceNodes}`;
@@ -44,8 +45,8 @@ const buildCommonQueryParams = (params: GraphUrlParams): string => {
   return q;
 };
 
-export const makeNamespacesGraphUrlFromParams = (params: GraphUrlParams, isPf = false): string => {
-  const route = isPf ? 'graphpf' : 'graph';
+export const makeNamespacesGraphUrlFromParams = (params: GraphUrlParams): string => {
+  const route = 'graphpf';
   let queryParams = buildCommonQueryParams(params);
   if (params.activeNamespaces.length > 0) {
     const namespaces = params.activeNamespaces.map(namespace => namespace.name).join(',');
@@ -58,8 +59,8 @@ export const makeNamespacesGraphUrlFromParams = (params: GraphUrlParams, isPf =
   return `/${route}/namespaces?${queryParams}`;
 };
 
-export const makeNodeGraphUrlFromParams = (params: GraphUrlParams, isPf = false): string => {
-  const route = isPf ? 'graphpf' : 'graph';
+export const makeNodeGraphUrlFromParams = (params: GraphUrlParams): string => {
+  const route = 'graphpf';
   const node = params.node;
   if (node) {
     switch (node.nodeType) {
@@ -91,7 +92,7 @@ export const makeNodeGraphUrlFromParams = (params: GraphUrlParams, isPf = false)
         )}`;
       default:
         console.debug('makeNodeUrl defaulting to makeNamespaceUrl');
-        return makeNamespacesGraphUrlFromParams(params, isPf);
+        return makeNamespacesGraphUrlFromParams(params);
     }
   } else {
     // this should never happen but typescript needs this
diff --git a/frontend/src/components/Nav/__tests__/RenderPage.test.tsx b/frontend/src/components/Nav/__tests__/RenderPage.test.tsx
index b03d0808f..81242144d 100644
--- a/frontend/src/components/Nav/__tests__/RenderPage.test.tsx
+++ b/frontend/src/components/Nav/__tests__/RenderPage.test.tsx
@@ -34,7 +34,7 @@ describe('RenderPage isGraph prop', () => {
     // we need a user session to render the navigation component
     store.dispatch(LoginActions.loginSuccess(session));
 
-    router.navigate('/graph/namespaces');
+    router.navigate('/graphpf/namespaces');
 
     const wrapper = mount(
       <Provider store={store}>
diff --git a/frontend/src/components/Pf/PfColors.tsx b/frontend/src/components/Pf/PfColors.tsx
index 35e12d4fd..b3e676b5c 100644
--- a/frontend/src/components/Pf/PfColors.tsx
+++ b/frontend/src/components/Pf/PfColors.tsx
@@ -2,11 +2,7 @@
 // PF colors, and moreover, use the defined color variables such that any changes made by PF are
 // picked up when the PF version is updated.  The preferred, standard way, is in CSS styling.  In
 // those cases we can directly let CSS resolve the PF var. So, whenever possible use the PFColors
-// enum below.  In certain cases (like in cytoscape), we need the explicit hex value.  In that case
-// we must actually get the computed value.  We do this as soon as we get an initial document (in
-// StartupInitializer.tsx). In those cases use PFColorVals.  Note that those values are not
-// available until they can be computed, so don't use them in constants or before they are
-// available.
+// enum below.
 
 // Colors used by Kiali for CSS styling
 export enum PFColors {
@@ -94,106 +90,3 @@ export enum PFColors {
   Color300 = 'var(--pf-v5-global--Color--300)',
   Color400 = 'var(--pf-v5-global--Color--400)'
 }
-
-// The hex string value of the PF CSS variable
-export type PFColorVal = string;
-
-// Color values used by Kiali outside of CSS (i.e. when we must have the actual hex value)
-/* eslint-disable @typescript-eslint/member-ordering*/
-export type PFColorValues = {
-  Black100: PFColorVal;
-  Black150: PFColorVal;
-  Black200: PFColorVal;
-  Black300: PFColorVal;
-  Black400: PFColorVal;
-  Black500: PFColorVal;
-  Black600: PFColorVal;
-  Black700: PFColorVal;
-  Black1000: PFColorVal;
-  Blue50: PFColorVal;
-  Blue300: PFColorVal;
-  Blue600: PFColorVal;
-  Red50: PFColorVal;
-  Orange50: PFColorVal;
-  Gold400: PFColorVal;
-  Green400: PFColorVal;
-  Purple200: PFColorVal;
-  White: PFColorVal;
-
-  // Health/Alert colors https://www.patternfly.org/v4/design-guidelines/styles/colors
-  Danger: PFColorVal;
-  Success: PFColorVal;
-  Warning: PFColorVal;
-
-  // PF colors (compatible with dark mode)
-  BackgroundColor100: PFColorVal;
-  BackgroundColor200: PFColorVal;
-
-  Color100: PFColorVal;
-  Color200: PFColorVal;
-
-  BorderColor100: PFColorVal;
-  BorderColor200: PFColorVal;
-  BorderColor300: PFColorVal;
-
-  // PF colors for dark backgrounds (don't change in dark mode)
-  Color300: PFColorVal;
-  Color400: PFColorVal;
-};
-
-export let PFColorVals: PFColorValues;
-
-/*
-  Extract color from var
-  Input : var(--pf-v5-global--palette--black-100)
-  Output:  --pf-v5-global--palette--black-100
-
-  - In case there is not var then return the same input
-*/
-const getColor = (val: string): string => {
-  return val.indexOf('var(') === 0 ? val.split('(').pop()!.split(')')[0] : val;
-};
-
-export const setPFColorVals = (element: Element): void => {
-  PFColorVals = {
-    // color values used by kiali
-    Black100: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black100)),
-    Black150: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black150)),
-    Black200: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black200)),
-    Black300: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black300)),
-    Black400: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black400)),
-    Black500: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black500)),
-    Black600: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black600)),
-    Black700: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black700)),
-    Black1000: getComputedStyle(element).getPropertyValue(getColor(PFColors.Black1000)),
-    Blue50: getComputedStyle(element).getPropertyValue(getColor(PFColors.Blue50)),
-    Blue300: getComputedStyle(element).getPropertyValue(getColor(PFColors.Blue300)),
-    Blue600: getComputedStyle(element).getPropertyValue(getColor(PFColors.Blue600)),
-    Red50: getComputedStyle(element).getPropertyValue(getColor(PFColors.Red50)),
-    Orange50: getComputedStyle(element).getPropertyValue(getColor(PFColors.Orange50)),
-    Gold400: getComputedStyle(element).getPropertyValue(getColor(PFColors.Gold400)),
-    Green400: getComputedStyle(element).getPropertyValue(getColor(PFColors.Green400)),
-    Purple200: getComputedStyle(element).getPropertyValue(getColor(PFColors.Purple200)),
-    White: getComputedStyle(element).getPropertyValue(getColor(PFColors.White)),
-
-    // status color values used by kiali
-    Danger: getComputedStyle(element).getPropertyValue(getColor(PFColors.Danger)),
-    Success: getComputedStyle(element).getPropertyValue(getColor(PFColors.Success)),
-    Warning: getComputedStyle(element).getPropertyValue(getColor(PFColors.Warning)),
-
-    // PF colors (compatible with dark mode)
-    BackgroundColor100: getComputedStyle(element).getPropertyValue(getColor(PFColors.BackgroundColor100)),
-    BackgroundColor200: getComputedStyle(element).getPropertyValue(getColor(PFColors.BackgroundColor200)),
-
-    Color100: getComputedStyle(element).getPropertyValue(getColor(PFColors.Color100)),
-    Color200: getComputedStyle(element).getPropertyValue(getColor(PFColors.Color200)),
-
-    BorderColor100: getComputedStyle(element).getPropertyValue(getColor(PFColors.BorderColor100)),
-    BorderColor200: getComputedStyle(element).getPropertyValue(getColor(PFColors.BorderColor200)),
-    BorderColor300: getComputedStyle(element).getPropertyValue(getColor(PFColors.BorderColor300)),
-
-    // PF colors for dark backgrounds (don't change in dark mode)
-    Color300: getComputedStyle(element).getPropertyValue(getColor(PFColors.Color300)),
-    Color400: getComputedStyle(element).getPropertyValue(getColor(PFColors.Color400))
-  };
-};
diff --git a/frontend/src/config/Config.ts b/frontend/src/config/Config.ts
index 81fa42c39..87fbc9dfe 100644
--- a/frontend/src/config/Config.ts
+++ b/frontend/src/config/Config.ts
@@ -167,6 +167,8 @@ const conf = {
         `api/namespaces/${namespace}/pods/${pod}/config_dump/${resource}`,
       podZtunnelConfig: (namespace: string, pod: string) =>
         `api/namespaces/${namespace}/pods/${pod}/config_dump_ztunnel`,
+      resourceUsageMetrics: (namespace: string, workload: string) =>
+        `api/namespaces/${namespace}/${workload}/usage_metrics`,
       serverConfig: `api/config`,
       service: (namespace: string, service: string) => `api/namespaces/${namespace}/services/${service}`,
       serviceGraphElements: (namespace: string, service: string) =>
@@ -193,8 +195,7 @@ const conf = {
       workloadDashboard: (namespace: string, workload: string) =>
         `api/namespaces/${namespace}/workloads/${workload}/dashboard`,
       ztunnelDashboard: (namespace: string, controlPlane: string) =>
-        `api/namespaces/${namespace}/ztunnel/${controlPlane}/dashboard`,
-      ztunnelMetrics: (namespace: string, workload: string) => `api/namespaces/${namespace}/ztunnel/${workload}/metrics`
+        `api/namespaces/${namespace}/ztunnel/${controlPlane}/dashboard`
     }
   },
   /** Graph configurations */
diff --git a/frontend/src/config/ServerConfig.ts b/frontend/src/config/ServerConfig.ts
index 6440fccd9..dafb55b0e 100644
--- a/frontend/src/config/ServerConfig.ts
+++ b/frontend/src/config/ServerConfig.ts
@@ -97,12 +97,8 @@ const defaultServerConfig: ComputedServerConfig = {
       graph: {
         findOptions: [],
         hideOptions: [],
-        impl: 'pf',
         settings: {
-          animation: 'point',
-          fontLabel: 13,
-          minFontBadge: 7,
-          minFontLabel: 10
+          animation: 'point'
         },
         traffic: {
           ambient: 'total',
diff --git a/frontend/src/helpers/GraphHelpers.tsx b/frontend/src/helpers/GraphHelpers.tsx
index fdfcab74c..eb5a59f54 100644
--- a/frontend/src/helpers/GraphHelpers.tsx
+++ b/frontend/src/helpers/GraphHelpers.tsx
@@ -1,5 +1,62 @@
 import { Controller, Edge, GraphElement, isEdge, isNode, Node, NodeModel } from '@patternfly/react-topology';
 import { action } from 'mobx';
+import { DecoratedGraphEdgeWrapper, DecoratedGraphElements, DecoratedGraphNodeWrapper } from 'types/Graph';
+
+export const toSafeFieldName = (fieldName: string): string => {
+  const alnumString = /^[a-zA-Z0-9]*$/;
+  const unsafeChar = /[^a-zA-Z0-9]/g;
+
+  if (fieldName.match(alnumString)) {
+    return fieldName;
+  }
+
+  return fieldName.replace(unsafeChar, '_');
+};
+
+// It is common that when updating the graph that the element topology (nodes, edges) remain the same,
+// only their activity changes (rates, etc). When the topology remains the same we may be able to optimize
+// some behavior.  This returns true if the topology changes, false otherwise.
+// 1) Quickly compare the number of nodes and edges, if different return true.
+// 2) Compare the ids
+export const elementsChanged = (
+  prevElements: DecoratedGraphElements,
+  nextElements: DecoratedGraphElements
+): boolean => {
+  if (prevElements === nextElements) {
+    return false;
+  }
+
+  if (
+    !prevElements ||
+    !nextElements ||
+    !prevElements.nodes ||
+    !prevElements.edges ||
+    !nextElements.nodes ||
+    !nextElements.edges ||
+    prevElements.nodes.length !== nextElements.nodes.length ||
+    prevElements.edges.length !== nextElements.edges.length
+  ) {
+    return true;
+  }
+
+  return !(
+    nodeOrEdgeArrayHasSameIds(nextElements.nodes, prevElements.nodes) &&
+    nodeOrEdgeArrayHasSameIds(nextElements.edges, prevElements.edges)
+  );
+};
+
+const nodeOrEdgeArrayHasSameIds = <T extends DecoratedGraphNodeWrapper | DecoratedGraphEdgeWrapper>(
+  a: Array<T>,
+  b: Array<T>
+): boolean => {
+  const aIds = a.map(e => e.data.id).sort();
+  return b
+    .map(e => e.data.id)
+    .sort()
+    .every((eId, index) => eId === aIds[index]);
+};
+
+//=======================================================================================================
 
 // TODO: When/if it is fixed this can be replaced with a straight call to node.getAllNodeChildren();
 // https://github.com/patternfly/patternfly-react/issues/8350
diff --git a/frontend/src/pages/AppDetails/AppInfo.tsx b/frontend/src/pages/AppDetails/AppInfo.tsx
index 707c633e8..981a81b52 100644
--- a/frontend/src/pages/AppDetails/AppInfo.tsx
+++ b/frontend/src/pages/AppDetails/AppInfo.tsx
@@ -7,11 +7,7 @@ import { DurationInSeconds } from 'types/Common';
 import { GraphDataSource } from 'services/GraphDataSource';
 import { AppHealth } from 'types/Health';
 import { kialiStyle } from 'styles/StyleUtils';
-import { GraphEdgeTapEvent } from '../../components/CytoscapeGraph/CytoscapeGraph';
-import { location, router, URLParam } from '../../app/History';
-import { MiniGraphCard } from '../../components/CytoscapeGraph/MiniGraphCard';
-import { serverConfig } from 'config';
-import { MiniGraphCardPF } from 'pages/GraphPF/MiniGraphCardPF';
+import { MiniGraphCardPF } from 'pages/Graph/MiniGraphCardPF';
 
 type AppInfoProps = {
   app?: App;
@@ -59,26 +55,11 @@ export class AppInfo extends React.Component<AppInfoProps, AppInfoState> {
     );
   };
 
-  goToMetrics = (e: GraphEdgeTapEvent): void => {
-    if (e.source !== e.target && this.props.app) {
-      const direction = e.source === this.props.app.name ? 'outbound' : 'inbound';
-      const destination = direction === 'inbound' ? 'source_canonical_service' : 'destination_canonical_service';
-      const urlParams = new URLSearchParams(location.getSearch());
-      urlParams.set('tab', direction === 'inbound' ? 'in_metrics' : 'out_metrics');
-      urlParams.set(URLParam.BY_LABELS, `${destination}=${e.source === this.props.app.name ? e.target : e.source}`);
-      router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true });
-    }
-  };
-
   render(): React.ReactNode {
     // RenderComponentScroll handles height to provide an inner scroll combined with tabs
     // This height needs to be propagated to minigraph to proper resize in height
     // Graph resizes correctly on width
-    const height = this.state.tabHeight ? this.state.tabHeight - 115 : 300;
-    const graphContainerStyle = kialiStyle({ width: '100%', height: height });
-    const includeMiniGraphCy = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'pf';
-    const includeMiniGraphPF = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'cy';
-    const miniGraphSpan = includeMiniGraphCy && includeMiniGraphPF ? 4 : 8;
+    const miniGraphSpan = 8;
     return (
       <RenderComponentScroll onResize={height => this.setState({ tabHeight: height })}>
         <Grid hasGutter={true} className={fullHeightStyle}>
@@ -86,21 +67,9 @@ export class AppInfo extends React.Component<AppInfoProps, AppInfoState> {
             <AppDescription app={this.props.app} health={this.props.health} isSupported={this.props.isSupported} />
           </GridItem>
 
-          {includeMiniGraphCy && (
-            <GridItem span={miniGraphSpan}>
-              <MiniGraphCard
-                onEdgeTap={this.goToMetrics}
-                dataSource={this.graphDataSource}
-                graphContainerStyle={graphContainerStyle}
-              />
-            </GridItem>
-          )}
-
-          {includeMiniGraphPF && (
-            <GridItem span={miniGraphSpan}>
-              <MiniGraphCardPF dataSource={this.graphDataSource} />
-            </GridItem>
-          )}
+          <GridItem span={miniGraphSpan}>
+            <MiniGraphCardPF dataSource={this.graphDataSource} />
+          </GridItem>
         </Grid>
       </RenderComponentScroll>
     );
diff --git a/frontend/src/pages/Graph/ContextMenu/NodeContextMenu.tsx b/frontend/src/pages/Graph/ContextMenu/NodeContextMenu.tsx
new file mode 100644
index 000000000..755ad00ce
--- /dev/null
+++ b/frontend/src/pages/Graph/ContextMenu/NodeContextMenu.tsx
@@ -0,0 +1,127 @@
+import { router } from 'app/History';
+import { BoxByType, DecoratedGraphNodeData, NodeType } from 'types/Graph';
+import { TracingInfo } from 'types/TracingInfo';
+import { isMultiCluster, Paths } from 'config';
+import { isParentKiosk, kioskContextMenuAction } from '../../../components/Kiosk/KioskActions';
+type LinkParams = { cluster?: string; name: string; namespace: string; type: string };
+
+const getLinkParamsForNode = (node: DecoratedGraphNodeData): LinkParams | undefined => {
+  const namespace: string = node.isServiceEntry ? node.isServiceEntry.namespace : node.namespace;
+
+  let cluster = node.cluster;
+  let name: string | undefined = undefined;
+  let type: string | undefined = undefined;
+
+  switch (node.nodeType) {
+    case NodeType.APP:
+    case NodeType.BOX:
+      // only app boxes have full context menus
+      const isBox = node.isBox;
+
+      if (!isBox || isBox === BoxByType.APP) {
+        // Prefer workload links
+        if (node.workload && node.parent) {
+          name = node.workload;
+          type = Paths.WORKLOADS;
+        } else {
+          type = Paths.APPLICATIONS;
+          name = node.app;
+        }
+      }
+      break;
+    case NodeType.SERVICE:
+      type = node.isServiceEntry ? Paths.SERVICEENTRIES : Paths.SERVICES;
+      name = node.service;
+      break;
+    case NodeType.WORKLOAD:
+      name = node.workload;
+      type = Paths.WORKLOADS;
+      break;
+  }
+
+  return type && name ? { namespace, type, name, cluster } : undefined;
+};
+
+const getTracingURL = (namespace: string, namespaceSelector: boolean, tracingURL: string, name?: string): string => {
+  return `${tracingURL}/search?service=${name}${namespaceSelector ? `.${namespace}` : ''}`;
+};
+
+export type ContextMenuOption = {
+  external?: boolean;
+  target?: string;
+  text: string;
+  url: string;
+};
+
+export const clickHandler = (o: ContextMenuOption, kiosk: string): void => {
+  if (o.external) {
+    window.open(o.url, o.target);
+  } else {
+    if (isParentKiosk(kiosk)) {
+      kioskContextMenuAction(o.url);
+    } else {
+      router.navigate(o.url);
+    }
+  }
+};
+
+export const getOptions = (node: DecoratedGraphNodeData, tracingInfo?: TracingInfo): ContextMenuOption[] => {
+  if (node.isInaccessible) {
+    return [];
+  }
+
+  const linkParams = getLinkParamsForNode(node);
+
+  if (!linkParams) {
+    return [];
+  }
+
+  return getOptionsFromLinkParams(linkParams, tracingInfo);
+};
+
+const getOptionsFromLinkParams = (linkParams: LinkParams, tracingInfo?: TracingInfo): ContextMenuOption[] => {
+  const { namespace, type, name, cluster } = linkParams;
+
+  let options: ContextMenuOption[] = [];
+  let detailsPageUrl = `/namespaces/${namespace}/${type}/${name}`;
+  let concat = '?';
+
+  if (cluster && isMultiCluster) {
+    detailsPageUrl += `?clusterName=${cluster}`;
+    concat = '&';
+  }
+
+  options.push({ text: 'Details', url: detailsPageUrl });
+
+  if (type !== Paths.SERVICEENTRIES) {
+    options.push({ text: 'Traffic', url: `${detailsPageUrl}${concat}tab=traffic` });
+
+    if (type === Paths.WORKLOADS) {
+      options.push({ text: 'Logs', url: `${detailsPageUrl}${concat}tab=logs` });
+    }
+
+    options.push({
+      text: 'Inbound Metrics',
+      url: `${detailsPageUrl}${concat}tab=${type === Paths.SERVICES ? 'metrics' : 'in_metrics'}`
+    });
+
+    if (type !== Paths.SERVICES) {
+      options.push({ text: 'Outbound Metrics', url: `${detailsPageUrl}${concat}tab=out_metrics` });
+    }
+
+    if (type === Paths.APPLICATIONS && tracingInfo && tracingInfo.enabled) {
+      if (tracingInfo.integration) {
+        options.push({ text: 'Traces', url: `${detailsPageUrl}${concat}tab=traces` });
+      } else if (tracingInfo.url) {
+        options.push({
+          text: 'Show Traces',
+          url: getTracingURL(namespace, tracingInfo.namespaceSelector, tracingInfo.url, name),
+          external: true,
+          target: '_blank'
+        });
+      }
+    }
+  }
+
+  return options;
+};
diff --git a/frontend/src/components/CytoscapeGraph/EmptyGraphLayout.tsx b/frontend/src/pages/Graph/EmptyGraphLayout.tsx
similarity index 100%
rename from frontend/src/components/CytoscapeGraph/EmptyGraphLayout.tsx
rename to frontend/src/pages/Graph/EmptyGraphLayout.tsx
diff --git a/frontend/src/pages/Graph/GraphHelpTour.tsx b/frontend/src/pages/Graph/GraphHelpTour.tsx
index ffcb08dfe..d9a4d2e34 100644
--- a/frontend/src/pages/Graph/GraphHelpTour.tsx
+++ b/frontend/src/pages/Graph/GraphHelpTour.tsx
@@ -1,6 +1,5 @@
 import { PopoverPosition } from '@patternfly/react-core';
 import { TourStopInfo, TourInfo } from 'components/Tour/TourStop';
-import { GraphShortcuts } from './GraphToolbar/GraphShortcuts';
 import { t } from 'utils/I18nUtils';
 import { GraphShortcutsPF } from './GraphToolbar/GraphShortcutsPF';
 
@@ -79,11 +78,6 @@ export const GraphTourStops: { [name: string]: TourStopInfo } = {
     description: t('Select the namespaces you want to see in the graph.'),
     position: PopoverPosition.bottomStart
   },
-  Shortcuts: {
-    name: t('Shortcuts'),
-    htmlDescription: GraphShortcuts,
-    position: PopoverPosition.leftStart
-  },
   ShortcutsPF: {
     name: t('Shortcuts'),
     htmlDescription: GraphShortcutsPF,
@@ -105,24 +99,6 @@ export const GraphTourStops: { [name: string]: TourStopInfo } = {
   }
 };
 
-export const GraphTour: TourInfo = {
-  name: t('GraphTour'),
-  stops: [
-    GraphTourStops.Shortcuts,
-    GraphTourStops.Namespaces,
-    GraphTourStops.GraphTraffic,
-    GraphTourStops.GraphType,
-    GraphTourStops.Display,
-    GraphTourStops.Find,
-    GraphTourStops.TimeRange,
-    GraphTourStops.Graph,
-    GraphTourStops.ContextualMenu,
-    GraphTourStops.SidePanel,
-    GraphTourStops.Layout,
-    GraphTourStops.Legend
-  ]
-};
-
 export const GraphTourPF: TourInfo = {
   name: t('GraphTour'),
   stops: [
diff --git a/frontend/src/pages/GraphPF/GraphHighlighterPF.ts b/frontend/src/pages/Graph/GraphHighlighterPF.ts
similarity index 99%
rename from frontend/src/pages/GraphPF/GraphHighlighterPF.ts
rename to frontend/src/pages/Graph/GraphHighlighterPF.ts
index 0f7f64683..01f5a72bb 100644
--- a/frontend/src/pages/GraphPF/GraphHighlighterPF.ts
+++ b/frontend/src/pages/Graph/GraphHighlighterPF.ts
@@ -1,6 +1,6 @@
 // There are three special states for a Node or Edge:
 //
-//   - selected: The node has been single-clicked and is 'selected' in cytoscape
+//   - selected: The node has been single-clicked and is 'selected' on the graph
 //   - hovered: The mouse is currently over the element.  It is marked with HoveredClass
 //   - highlighted: The element is to be emphasized (see below).  It is marked with HighlightClass.
 //   - unhighlighted: The element is to be de-emphasized (see below).  It is marked with UnhighlightClass.
diff --git a/frontend/src/pages/GraphPF/GraphLegendDataPF.ts b/frontend/src/pages/Graph/GraphLegendDataPF.ts
similarity index 100%
rename from frontend/src/pages/GraphPF/GraphLegendDataPF.ts
rename to frontend/src/pages/Graph/GraphLegendDataPF.ts
diff --git a/frontend/src/pages/GraphPF/GraphLegendPF.tsx b/frontend/src/pages/Graph/GraphLegendPF.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/GraphLegendPF.tsx
rename to frontend/src/pages/Graph/GraphLegendPF.tsx
diff --git a/frontend/src/pages/GraphPF/GraphPF.tsx b/frontend/src/pages/Graph/GraphPF.tsx
similarity index 90%
rename from frontend/src/pages/GraphPF/GraphPF.tsx
rename to frontend/src/pages/Graph/GraphPF.tsx
index 17505dc78..65fe9bb0e 100644
--- a/frontend/src/pages/GraphPF/GraphPF.tsx
+++ b/frontend/src/pages/Graph/GraphPF.tsx
@@ -28,13 +28,11 @@ import {
   GRAPH_AREA_SELECTED_EVENT,
   GraphLayoutEndEventListener
 } from '@patternfly/react-topology';
-import { GraphData } from 'pages/Graph/GraphPage';
 import {
   BoxByType,
   EdgeLabelMode,
   EdgeMode,
   GraphEvent,
-  Layout,
   NodeAttr,
   NodeType,
   RankMode,
@@ -43,7 +41,7 @@ import {
 } from 'types/Graph';
 import { JaegerTrace } from 'types/TracingInfo';
 import { stylesComponentFactory } from './components/stylesComponentFactory';
-import { elementFactory } from './elements/elementFactory';
+import { elementFactory } from '../Graph/elements/elementFactory';
 import {
   assignEdgeHealth,
   EdgeData,
@@ -60,22 +58,18 @@ import { layoutFactory } from './layouts/layoutFactory';
 import { hideTrace, showTrace } from './TracePF';
 import { GraphHighlighterPF } from './GraphHighlighterPF';
 import { TimeInMilliseconds } from 'types/Common';
-import { KialiConcentricGraph } from 'components/CytoscapeGraph/graphs/KialiConcentricGraph';
-import { KialiDagreGraph } from 'components/CytoscapeGraph/graphs/KialiDagreGraph';
-import { KialiGridGraph } from 'components/CytoscapeGraph/graphs/KialiGridGraph';
-import { KialiBreadthFirstGraph } from 'components/CytoscapeGraph/graphs/KialiBreadthFirstGraph';
 import { HistoryManager, URLParam } from 'app/History';
 import { TourStop } from 'components/Tour/TourStop';
 import { GraphTourStops } from 'pages/Graph/GraphHelpTour';
 import { supportsGroups } from 'utils/GraphUtils';
-import { GraphRefs } from './GraphPagePF';
+import { GraphData, GraphRefs } from './GraphPagePF';
 import { WizardAction, WizardMode } from 'components/IstioWizards/WizardActions';
 import { ServiceDetailsInfo } from 'types/ServiceInfo';
 import { PeerAuthentication } from 'types/IstioObjects';
 import { KialiIcon } from 'config/KialiIcon';
 import { toolbarActiveStyle } from 'styles/GraphStyle';
-import { scoreNodes, ScoringCriteria } from 'components/CytoscapeGraph/GraphScore';
-import { TrafficAnimation } from './TrafficAnimation/TrafficRendererPF';
+import { scoreNodes, ScoringCriteria } from 'pages/Graph/GraphScore';
+import { TrafficAnimation } from '../Graph/TrafficAnimation/TrafficRendererPF';
 
 const DEFAULT_NODE_SIZE = 40;
 const ZOOM_IN = 4 / 3;
@@ -83,13 +77,6 @@ const ZOOM_OUT = 3 / 4;
 
 export const FIT_PADDING = 90;
 
-export enum LayoutName {
-  BreadthFirst = 'BreadthFirst',
-  Concentric = 'Concentric',
-  Dagre = 'Dagre',
-  Grid = 'Grid'
-}
-
 export enum LayoutType {
   Layout = 'layout',
   LayoutNoFit = 'layoutNoFit',
@@ -99,16 +86,24 @@ export enum LayoutType {
 let initialLayout = false;
 let layoutInProgress: LayoutType | undefined;
 
-export function getLayoutByName(layoutName: string): Layout {
-  switch (layoutName) {
-    case LayoutName.BreadthFirst:
-      return KialiBreadthFirstGraph.getLayout();
-    case LayoutName.Concentric:
-      return KialiConcentricGraph.getLayout();
-    case LayoutName.Grid:
-      return KialiGridGraph.getLayout();
+// the layouts offered by the traffic graph page
+export enum GraphLayout {
+  BreadthFirst = 'breadthfirst',
+  Concentric = 'concentric',
+  Dagre = 'dagre',
+  Grid = 'grid'
+}
+
+export function getValidGraphLayout(layout: string): GraphLayout {
+  switch (layout) {
+    case GraphLayout.BreadthFirst:
+      return GraphLayout.BreadthFirst;
+    case GraphLayout.Concentric:
+      return GraphLayout.Concentric;
+    case GraphLayout.Grid:
+      return GraphLayout.Grid;
     default:
-      return KialiDagreGraph.getLayout();
+      return GraphLayout.Dagre;
   }
 }
 
@@ -147,7 +142,7 @@ const TopologyContent: React.FC<{
   graphData: GraphData;
   highlighter: GraphHighlighterPF;
   isMiniGraph: boolean;
-  layoutName: LayoutName;
+  layoutName: GraphLayout;
   onDeleteTrafficRouting: (key: string, serviceDetails: ServiceDetailsInfo) => void;
   onEdgeTap?: (edge: Edge<EdgeModel>) => void;
   onLaunchWizard: (
@@ -162,7 +157,7 @@ const TopologyContent: React.FC<{
   onReady: (refs: GraphRefs) => void;
   rankBy: RankMode[];
   setEdgeMode: (edgeMode: EdgeMode) => void;
-  setLayout: (val: LayoutName) => void;
+  setLayout: (val: GraphLayout) => void;
   setRankResult: (rankResult: RankResult) => void;
   setUpdateTime: (val: TimeInMilliseconds) => void;
   showLegend: boolean;
@@ -256,7 +251,7 @@ const TopologyContent: React.FC<{
             return;
           }
           default:
-            updateSummary({ isPF: true, summaryType: 'graph', summaryTarget: controller } as GraphEvent);
+            updateSummary({ summaryType: 'graph', summaryTarget: controller } as GraphEvent);
         }
       }
       return;
@@ -267,25 +262,25 @@ const TopologyContent: React.FC<{
       switch (elem?.getKind()) {
         case ModelKind.edge: {
           highlighter.setSelectedId(selectedIds[0]);
-          updateSummary({ isPF: true, summaryType: 'edge', summaryTarget: elem } as GraphEvent);
+          updateSummary({ summaryType: 'edge', summaryTarget: elem } as GraphEvent);
           return;
         }
         case ModelKind.node: {
           highlighter.setSelectedId(selectedIds[0]);
           const isBox = (elem.getData() as NodeData).isBox;
-          updateSummary({ isPF: true, summaryType: isBox ? 'box' : 'node', summaryTarget: elem } as GraphEvent);
+          updateSummary({ summaryType: isBox ? 'box' : 'node', summaryTarget: elem } as GraphEvent);
           return;
         }
         case ModelKind.graph:
         default:
           highlighter.setSelectedId(undefined);
           setSelectedIds([]);
-          updateSummary({ isPF: true, summaryType: 'graph', summaryTarget: controller } as GraphEvent);
+          updateSummary({ summaryType: 'graph', summaryTarget: controller } as GraphEvent);
           return;
       }
     } else {
       highlighter.setSelectedId(undefined);
-      updateSummary({ isPF: true, summaryType: 'graph', summaryTarget: controller } as GraphEvent);
+      updateSummary({ summaryType: 'graph', summaryTarget: controller } as GraphEvent);
     }
   }, [controller, highlighter, isMiniGraph, onEdgeTap, onNodeTap, selectedIds, setSelectedIds, updateSummary]);
 
@@ -730,10 +725,10 @@ const TopologyContent: React.FC<{
   React.useEffect(() => {
     return () => {
       if (updateSummary) {
-        updateSummary({ isPF: true, summaryType: 'graph', summaryTarget: undefined });
+        updateSummary({ summaryType: 'graph', summaryTarget: controller });
       }
     };
-  }, [updateSummary]);
+  }, [controller, updateSummary]);
 
   // Enable the selection-based zoom
   useEventListener<GraphAreaSelectedEventListener>(
@@ -801,12 +796,12 @@ const TopologyContent: React.FC<{
                       id: 'toolbar_layout_dagre',
                       icon: (
                         <KialiIcon.Topology
-                          className={LayoutName.Dagre === layoutName ? toolbarActiveStyle : undefined}
+                          className={GraphLayout.Dagre === layoutName ? toolbarActiveStyle : undefined}
                         />
                       ),
                       tooltip: 'Dagre - boxing layout',
                       callback: () => {
-                        setLayoutName(LayoutName.Dagre);
+                        setLayoutName(GraphLayout.Dagre);
                       }
                     },
                     {
@@ -814,12 +809,12 @@ const TopologyContent: React.FC<{
                       id: 'toolbar_layout_grid',
                       icon: (
                         <KialiIcon.Topology
-                          className={LayoutName.Grid === layoutName ? toolbarActiveStyle : undefined}
+                          className={GraphLayout.Grid === layoutName ? toolbarActiveStyle : undefined}
                         />
                       ),
                       tooltip: 'Grid - non-boxing layout',
                       callback: () => {
-                        setLayoutName(LayoutName.Grid);
+                        setLayoutName(GraphLayout.Grid);
                       }
                     },
                     {
@@ -827,12 +822,12 @@ const TopologyContent: React.FC<{
                       id: 'toolbar_layout_concentric',
                       icon: (
                         <KialiIcon.Topology
-                          className={LayoutName.Concentric === layoutName ? toolbarActiveStyle : undefined}
+                          className={GraphLayout.Concentric === layoutName ? toolbarActiveStyle : undefined}
                         />
                       ),
                       tooltip: 'Concentric - non-boxing layout',
                       callback: () => {
-                        setLayoutName(LayoutName.Concentric);
+                        setLayoutName(GraphLayout.Concentric);
                       }
                     },
                     {
@@ -840,12 +835,12 @@ const TopologyContent: React.FC<{
                       id: 'toolbar_layout_breadth_first',
                       icon: (
                         <KialiIcon.Topology
-                          className={LayoutName.BreadthFirst === layoutName ? toolbarActiveStyle : undefined}
+                          className={GraphLayout.BreadthFirst === layoutName ? toolbarActiveStyle : undefined}
                         />
                       ),
                       tooltip: 'Breadth First - non-boxing layout',
                       callback: () => {
-                        setLayoutName(LayoutName.BreadthFirst);
+                        setLayoutName(GraphLayout.BreadthFirst);
                       }
                     }
                   ],
@@ -888,7 +883,7 @@ export const GraphPF: React.FC<{
   focusNode?: FocusNode;
   graphData: GraphData;
   isMiniGraph: boolean;
-  layout: Layout;
+  layout: GraphLayout;
   onDeleteTrafficRouting: (key: string, serviceDetails: ServiceDetailsInfo) => void;
   onEdgeTap?: (edge: Edge<EdgeModel>) => void;
   onLaunchWizard: (
@@ -903,7 +898,7 @@ export const GraphPF: React.FC<{
   onReady: (refs: GraphRefs) => void;
   rankBy: RankMode[];
   setEdgeMode: (edgeMode: EdgeMode) => void;
-  setLayout: (layout: Layout) => void;
+  setLayout: (layout: GraphLayout) => void;
   setRankResult: (rankResult: RankResult) => void;
   setUpdateTime: (val: TimeInMilliseconds) => void;
   showLegend: boolean;
@@ -960,25 +955,12 @@ export const GraphPF: React.FC<{
     setTrafficAnimation(new TrafficAnimation(c));
   }, []);
 
-  const getLayoutName = (layout: Layout): LayoutName => {
-    switch (layout.name) {
-      case 'kiali-breadthfirst':
-        return LayoutName.BreadthFirst;
-      case 'kiali-concentric':
-        return LayoutName.Concentric;
-      case 'kiali-grid':
-        return LayoutName.Grid;
-      default:
-        return LayoutName.Dagre;
-    }
-  };
-
-  const setLayoutByName = (layoutName: LayoutName): void => {
-    const layout = getLayoutByName(layoutName);
-    HistoryManager.setParam(URLParam.GRAPH_LAYOUT, layout.name);
+  const setLayoutByName = (layout: GraphLayout): void => {
+    const validLayout = getValidGraphLayout(layout);
+    HistoryManager.setParam(URLParam.GRAPH_LAYOUT, validLayout);
     // TODO: PF graph does have support for namespace box layout, just use dagre
-    HistoryManager.setParam(URLParam.GRAPH_NAMESPACE_LAYOUT, KialiDagreGraph.getLayout().name);
-    setLayout(layout);
+    HistoryManager.setParam(URLParam.GRAPH_NAMESPACE_LAYOUT, GraphLayout.Dagre);
+    setLayout(validLayout);
   };
 
   if (!controller || !graphData || graphData.isLoading) {
@@ -1000,7 +982,7 @@ export const GraphPF: React.FC<{
         graphData={graphData}
         highlighter={highlighter!}
         isMiniGraph={isMiniGraph}
-        layoutName={getLayoutName(layout)}
+        layoutName={layout}
         onDeleteTrafficRouting={onDeleteTrafficRouting}
         onEdgeTap={onEdgeTap}
         onLaunchWizard={onLaunchWizard}
diff --git a/frontend/src/pages/GraphPF/GraphPFElems.tsx b/frontend/src/pages/Graph/GraphPFElems.tsx
similarity index 97%
rename from frontend/src/pages/GraphPF/GraphPFElems.tsx
rename to frontend/src/pages/Graph/GraphPFElems.tsx
index 1022a25cb..7f778679e 100644
--- a/frontend/src/pages/GraphPF/GraphPFElems.tsx
+++ b/frontend/src/pages/Graph/GraphPFElems.tsx
@@ -35,13 +35,12 @@ import { getEdgeHealth } from 'types/ErrorRate/GraphEdgeStatus';
 import { Span } from 'types/TracingInfo';
 import { IconType } from 'config/Icons';
 import { NodeDecorator } from './NodeDecorator';
-import { LayoutName } from './GraphPF';
+import { GraphLayout } from './GraphPF';
 import { supportsGroups } from 'utils/GraphUtils';
 import { kialiStyle } from 'styles/StyleUtils';
-import { TrafficPointGenerator } from './TrafficAnimation/TrafficRendererPF';
+import { TrafficPointGenerator } from '../Graph/TrafficAnimation/TrafficRendererPF';
 
 // Utilities for working with PF Topology
-// - most of these add cytoscape-like functions
 
 export type NodeMap = Map<string, NodeModel>;
 
@@ -162,7 +161,7 @@ const getDecorator = (element: Node, quadrant: TopologyQuadrant, icon: IconType,
 
 export const setNodeAttachments = (node: Node<NodeModel>, settings: GraphPFSettings): void => {
   // PFT provides the ability to add a single Icon (badge) on the label. And so we will use
-  // attachments (up to 4) to display what we would have done with label badges in Cytoscape.
+  // attachments (up to 4) to display things that we'd prefer to have shown with more icons.
   const data = node.getData() as NodeData;
   const attachments = [] as React.ReactNode[];
 
@@ -228,7 +227,7 @@ export const setNodeLabel = (
   node: NodeModel,
   nodeMap: NodeMap,
   settings: GraphPFSettings,
-  layoutName: LayoutName
+  layoutName: GraphLayout
 ): void => {
   const data = node.data as NodeData;
   const app = data.app ?? '';
@@ -259,9 +258,8 @@ export const setNodeLabel = (
   // Icon Badges portion of label...
 
   // PFT provides the ability to add a single Icon (badge) on the label. Given that we can't
-  // duplicate what we do with Cytoscape, which is to add multiple badges on the label,
-  // we'll reserve the single icon to be used only to identify traffic sources (i.e. roots).
-  // Note that a gateway is a special traffic source.
+  // do what we'd like, which is to use multiple icons,  we'll reserve the single icon to be
+  // used only to identify traffic sources (i.e. roots). Note that a gateway is a special traffic source.
   // Other badges will be added as attachments (decorators) on the node, but that requires
   // the Node, not the NodeModel, and it;s no longer part of the label, so it's not done here.
 
diff --git a/frontend/src/pages/Graph/GraphPage.tsx b/frontend/src/pages/Graph/GraphPage.tsx
deleted file mode 100644
index b69050587..000000000
--- a/frontend/src/pages/Graph/GraphPage.tsx
+++ /dev/null
@@ -1,970 +0,0 @@
-import * as Cy from 'cytoscape';
-import * as React from 'react';
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-import FlexView from 'react-flexview';
-import { kialiStyle } from 'styles/StyleUtils';
-import { router } from '../../app/History';
-import { DurationInSeconds, IntervalInMilliseconds, TimeInMilliseconds, TimeInSeconds } from '../../types/Common';
-import { MessageType } from '../../types/MessageCenter';
-import { Namespace } from '../../types/Namespace';
-import {
-  GraphEvent,
-  DecoratedGraphElements,
-  EdgeLabelMode,
-  GraphDefinition,
-  GraphType,
-  Layout,
-  NodeParamsType,
-  NodeType,
-  SummaryData,
-  UNKNOWN,
-  BoxByType,
-  TrafficRate,
-  RankMode,
-  RankResult,
-  EdgeMode
-} from '../../types/Graph';
-import { computePrometheusRateParams } from '../../services/Prometheus';
-import * as AlertUtils from '../../utils/AlertUtils';
-import { CytoscapeGraph, GraphNodeDoubleTapEvent } from '../../components/CytoscapeGraph/CytoscapeGraph';
-import { CytoscapeToolbar } from '../../components/CytoscapeGraph/CytoscapeToolbar';
-import { ErrorBoundary } from '../../components/ErrorBoundary/ErrorBoundary';
-import { GraphUrlParams, makeNodeGraphUrlFromParams } from '../../components/Nav/NavUtils';
-import { GraphToolbar } from './GraphToolbar/GraphToolbar';
-import { GraphLegend } from './GraphLegend';
-import { EmptyGraphLayout } from '../../components/CytoscapeGraph/EmptyGraphLayout';
-import { SummaryPanel } from './SummaryPanel';
-import {
-  activeNamespacesSelector,
-  durationSelector,
-  edgeLabelsSelector,
-  edgeModeSelector,
-  findValueSelector,
-  graphTypeSelector,
-  hideValueSelector,
-  meshWideMTLSEnabledSelector,
-  refreshIntervalSelector,
-  replayActiveSelector,
-  replayQueryTimeSelector,
-  trafficRatesSelector
-} from '../../store/Selectors';
-import { KialiAppState } from '../../store/Store';
-import { GraphActions } from '../../actions/GraphActions';
-import { GraphToolbarActions } from '../../actions/GraphToolbarActions';
-import { NodeContextMenu } from '../../components/CytoscapeGraph/ContextMenu/NodeContextMenu';
-import { PFColors } from 'components/Pf/PfColors';
-import { TourActions } from 'actions/TourActions';
-import { arrayEquals } from 'utils/Common';
-import { isKioskMode, getFocusSelector, unsetFocusSelector, getTraceId, getClusterName } from 'utils/SearchParamUtils';
-import { Badge, Chip } from '@patternfly/react-core';
-import { toRangeString } from 'components/Time/Utils';
-import { replayBorder } from 'components/Time/Replay';
-import { GraphDataSource, FetchParams, EMPTY_GRAPH_DATA } from '../../services/GraphDataSource';
-import { NamespaceActions } from '../../actions/NamespaceAction';
-import { GraphThunkActions } from '../../actions/GraphThunkActions';
-import { JaegerTrace } from 'types/TracingInfo';
-import { KialiDispatch } from 'types/Redux';
-import { TracingThunkActions } from 'actions/TracingThunkActions';
-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 { isMultiCluster } from 'config';
-
-// GraphURLPathProps holds path variable values.  Currently all path variables are relevant only to a node graph
-export type GraphURLPathProps = {
-  aggregate: string;
-  aggregateValue: string;
-  app: string;
-  namespace: string;
-  service: string;
-  version: string;
-  workload: string;
-};
-
-type ReduxDispatchProps = {
-  endTour: () => void;
-  onNamespaceChange: () => void;
-  onReady: (cytoscapeRef: any) => void;
-  setActiveNamespaces: (namespaces: Namespace[]) => void;
-  setGraphDefinition: (graphDefinition: GraphDefinition) => void;
-  setNode: (node?: NodeParamsType) => void;
-  setRankResult: (result: RankResult) => void;
-  setTraceId: (traceId?: string) => void;
-  setUpdateTime: (val: TimeInMilliseconds) => void;
-  startTour: ({ info, stop }) => void;
-  toggleIdleNodes: () => void;
-  toggleLegend: () => void;
-  updateSummary: (event: GraphEvent) => void;
-};
-
-type ReduxStateProps = {
-  activeNamespaces: Namespace[];
-  activeTour?: TourInfo;
-  boxByCluster: boolean;
-  boxByNamespace: boolean;
-  duration: DurationInSeconds; // current duration (dropdown) setting
-  edgeLabels: EdgeLabelMode[];
-  edgeMode: EdgeMode;
-  findValue: string;
-  graphType: GraphType;
-  hideValue: string;
-  isPageVisible: boolean;
-  istioAPIEnabled: boolean;
-  kiosk: string;
-  layout: Layout;
-  mtlsEnabled: boolean;
-  namespaceLayout: Layout;
-  node?: NodeParamsType;
-  rankBy: RankMode[];
-  refreshInterval: IntervalInMilliseconds;
-  replayActive: boolean;
-  replayQueryTime: TimeInMilliseconds;
-  showIdleEdges: boolean;
-  showIdleNodes: boolean;
-  showLegend: boolean;
-  showOperationNodes: boolean;
-  showOutOfMesh: boolean;
-  showRank: boolean;
-  showSecurity: boolean;
-  showServiceNodes: boolean;
-  showTrafficAnimation: boolean;
-  showVirtualServices: boolean;
-  showWaypoints: boolean;
-  summaryData: SummaryData | null;
-  theme: string;
-  trace?: JaegerTrace;
-  trafficRates: TrafficRate[];
-};
-
-type ReduxProps = ReduxStateProps & ReduxDispatchProps;
-
-export type GraphPageProps = Partial<GraphURLPathProps> &
-  ReduxProps & {
-    lastRefreshAt: TimeInMilliseconds;
-  };
-
-export type GraphData = {
-  elements: DecoratedGraphElements;
-  elementsChanged: boolean; // true if current elements differ from previous fetch, can be used as an optimization.
-  errorMessage?: string;
-  fetchParams: FetchParams;
-  isError?: boolean;
-  isLoading: boolean;
-  timestamp: TimeInMilliseconds;
-};
-
-type WizardsData = {
-  // Data (payload) sent to the wizard or the confirm delete dialog
-  gateways: string[];
-  k8sGateways: string[];
-  namespace: string;
-  peerAuthentications: PeerAuthentication[];
-  serviceDetails?: ServiceDetailsInfo;
-  // Wizard configuration
-  showWizard: boolean;
-  updateMode: boolean;
-  wizardType: string;
-};
-
-type GraphPageState = {
-  graphData: GraphData;
-  showConfirmDeleteTrafficRouting: boolean;
-  wizardsData: WizardsData;
-};
-
-const NUMBER_OF_DATAPOINTS = 30;
-
-const containerStyle = kialiStyle({
-  minHeight: '350px',
-  // TODO: try flexbox to remove this calc
-  height: 'calc(100vh - 113px)' // View height minus top bar height minus secondary masthead
-});
-
-const kioskContainerStyle = kialiStyle({
-  minHeight: '350px',
-  height: 'calc(100vh - 0.5rem)' // View height minus top bar height
-});
-
-const cytoscapeGraphContainerStyle = kialiStyle({ flex: '1', minWidth: '350px', zIndex: 0, paddingRight: '0.25rem' });
-const cytoscapeGraphWrapperDivStyle = kialiStyle({
-  position: 'relative',
-  backgroundColor: PFColors.BackgroundColor200
-});
-const cytoscapeToolbarWrapperDivStyle = kialiStyle({
-  position: 'absolute',
-  bottom: '0.25rem',
-  zIndex: 2,
-  borderStyle: 'hidden'
-});
-
-const graphTimeRange = kialiStyle({
-  position: 'absolute',
-  top: '0.5rem',
-  left: '0.5rem',
-  width: 'auto',
-  zIndex: 2
-});
-
-const replayBackground = kialiStyle({
-  backgroundColor: PFColors.Replay
-});
-
-const graphBackground = kialiStyle({
-  backgroundColor: PFColors.BackgroundColor100
-});
-
-const graphLegendStyle = kialiStyle({
-  right: '0',
-  bottom: '0.5rem',
-  position: 'absolute',
-  overflow: 'hidden'
-});
-
-const GraphErrorBoundaryFallback = (): React.ReactElement => {
-  return (
-    <div className={cytoscapeGraphContainerStyle}>
-      <EmptyGraphLayout
-        isError={true}
-        isMiniGraph={false}
-        namespaces={[]}
-        showIdleNodes={false}
-        toggleIdleNodes={() => undefined}
-      />
-    </div>
-  );
-};
-
-class GraphPageComponent extends React.Component<GraphPageProps, GraphPageState> {
-  private readonly errorBoundaryRef: any;
-  private cytoscapeGraphRef: any;
-  private focusSelector?: string;
-  private graphDataSource: GraphDataSource;
-
-  static getNodeParamsFromProps(props: Partial<GraphURLPathProps>): NodeParamsType | undefined {
-    const aggregate = props.aggregate;
-    const aggregateOk = aggregate && aggregate !== UNKNOWN;
-    const aggregateValue = props.aggregateValue;
-    const aggregateValueOk = aggregateValue && aggregateValue !== UNKNOWN;
-    const app = props.app;
-    const appOk = app && app !== UNKNOWN;
-    const namespace = props.namespace;
-    const namespaceOk = namespace && namespace !== UNKNOWN;
-    const service = props.service;
-    const serviceOk = service && service !== UNKNOWN;
-    const workload = props.workload;
-    const workloadOk = workload && workload !== UNKNOWN;
-
-    if (!aggregateOk && !aggregateValueOk && !appOk && !namespaceOk && !serviceOk && !workloadOk) {
-      // @ts-ignore
-      return;
-    }
-
-    let nodeType: NodeType;
-    let version: string | undefined;
-
-    if (aggregateOk) {
-      nodeType = NodeType.AGGREGATE;
-      version = '';
-    } else if (appOk || workloadOk) {
-      nodeType = appOk ? NodeType.APP : NodeType.WORKLOAD;
-      version = props.version;
-    } else {
-      nodeType = NodeType.SERVICE;
-      version = '';
-    }
-
-    return {
-      aggregate: aggregate!,
-      aggregateValue: aggregateValue!,
-      app: app!,
-      cluster: getClusterName(),
-      namespace: { name: namespace! },
-      nodeType: nodeType,
-      service: service!,
-      version: version,
-      workload: workload!
-    };
-  }
-
-  static isNodeChanged(prevNode?: NodeParamsType, node?: NodeParamsType): boolean {
-    if (prevNode === node) {
-      return false;
-    }
-
-    if ((prevNode && !node) || (!prevNode && node)) {
-      return true;
-    }
-
-    if (prevNode && node) {
-      const nodeAggregateHasChanged =
-        prevNode.aggregate !== node.aggregate || prevNode.aggregateValue !== node.aggregateValue;
-      const nodeAppHasChanged = prevNode.app !== node.app;
-      const nodeServiceHasChanged = prevNode.service !== node.service;
-      const nodeVersionHasChanged = prevNode.version !== node.version;
-      const nodeTypeHasChanged = prevNode.nodeType !== node.nodeType;
-      const nodeWorkloadHasChanged = prevNode.workload !== node.workload;
-
-      return (
-        nodeAggregateHasChanged ||
-        nodeAppHasChanged ||
-        nodeServiceHasChanged ||
-        nodeVersionHasChanged ||
-        nodeWorkloadHasChanged ||
-        nodeTypeHasChanged
-      );
-    }
-
-    return false;
-  }
-
-  constructor(props: GraphPageProps) {
-    super(props);
-    this.errorBoundaryRef = React.createRef();
-    this.cytoscapeGraphRef = React.createRef();
-    this.focusSelector = getFocusSelector();
-
-    this.graphDataSource = new GraphDataSource();
-
-    this.state = {
-      graphData: {
-        elements: { edges: [], nodes: [] },
-        elementsChanged: false,
-        fetchParams: this.graphDataSource.fetchParameters,
-        isLoading: true,
-        timestamp: 0
-      },
-      wizardsData: {
-        showWizard: false,
-        wizardType: '',
-        updateMode: false,
-        gateways: [],
-        k8sGateways: [],
-        peerAuthentications: [],
-        namespace: ''
-      },
-      showConfirmDeleteTrafficRouting: false
-    };
-  }
-
-  componentDidMount(): void {
-    // Connect to graph data source updates
-    this.graphDataSource.on('loadStart', this.handleGraphDataSourceStart);
-    this.graphDataSource.on('fetchError', this.handleGraphDataSourceError);
-    this.graphDataSource.on('fetchSuccess', this.handleGraphDataSourceSuccess);
-    this.graphDataSource.on('emptyNamespaces', this.handleGraphDataSourceEmpty);
-
-    // Let URL override current redux state at mount time.  We usually do this in
-    // the constructor but it seems to work better here when the initial URL
-    // is for a node graph.  When setting the node here it is available for the
-    // loadGraphFromBackend() call.
-    const urlNode = GraphPageComponent.getNodeParamsFromProps(this.props);
-
-    if (GraphPageComponent.isNodeChanged(urlNode, this.props.node)) {
-      // add the node namespace if necessary, but don't lose previously selected namespaces
-      if (urlNode && !this.props.activeNamespaces.map(ns => ns.name).includes(urlNode.namespace.name)) {
-        this.props.setActiveNamespaces([urlNode.namespace, ...this.props.activeNamespaces]);
-      }
-      this.props.setNode(urlNode);
-    }
-
-    const urlTrace = getTraceId();
-    if (urlTrace !== this.props.trace?.traceID) {
-      this.props.setTraceId(urlTrace);
-    }
-
-    // Ensure we initialize the graph. We wait for the toolbar to render and
-    // ensure all redux props are updated with URL settings.
-    // That in turn ensures the initial fetchParams are correct.
-    setTimeout(() => this.loadGraphDataFromBackend(), 0);
-  }
-
-  componentDidUpdate(prev: GraphPageProps): void {
-    const curr = this.props;
-
-    const activeNamespacesChanged = !arrayEquals(
-      prev.activeNamespaces,
-      curr.activeNamespaces,
-      (n1, n2) => n1.name === n2.name
-    );
-
-    // Ensure we initialize the graph when there is a change to activeNamespaces.
-    if (activeNamespacesChanged) {
-      this.props.onNamespaceChange();
-    }
-    if (
-      activeNamespacesChanged ||
-      prev.boxByCluster !== curr.boxByCluster ||
-      prev.boxByNamespace !== curr.boxByNamespace ||
-      prev.duration !== curr.duration ||
-      (prev.edgeLabels !== curr.edgeLabels && // test for edge labels that invoke graph gen appenders
-        (curr.edgeLabels.includes(EdgeLabelMode.RESPONSE_TIME_GROUP) ||
-          curr.edgeLabels.includes(EdgeLabelMode.THROUGHPUT_GROUP))) ||
-      (prev.findValue !== curr.findValue && curr.findValue.includes('label:')) ||
-      prev.graphType !== curr.graphType ||
-      (prev.hideValue !== curr.hideValue && curr.hideValue.includes('label:')) ||
-      (prev.lastRefreshAt !== curr.lastRefreshAt && curr.replayQueryTime === 0) ||
-      (prev.replayActive !== curr.replayActive && !curr.replayActive) ||
-      prev.replayQueryTime !== curr.replayQueryTime ||
-      prev.showIdleEdges !== curr.showIdleEdges ||
-      prev.showOperationNodes !== curr.showOperationNodes ||
-      prev.showServiceNodes !== curr.showServiceNodes ||
-      prev.showSecurity !== curr.showSecurity ||
-      prev.showIdleNodes !== curr.showIdleNodes ||
-      prev.showWaypoints !== curr.showWaypoints ||
-      prev.trafficRates !== curr.trafficRates ||
-      GraphPageComponent.isNodeChanged(prev.node, curr.node)
-    ) {
-      this.loadGraphDataFromBackend();
-    }
-
-    if (this.focusSelector) {
-      this.focusSelector = undefined;
-      unsetFocusSelector();
-    }
-
-    if (
-      prev.layout.name !== curr.layout.name ||
-      prev.namespaceLayout.name !== curr.namespaceLayout.name ||
-      activeNamespacesChanged
-    ) {
-      this.errorBoundaryRef.current.cleanError();
-    }
-
-    if (curr.showLegend && this.props.activeTour) {
-      this.props.endTour();
-    }
-  }
-
-  componentWillUnmount(): void {
-    // Disconnect from graph data source updates
-    this.graphDataSource.removeListener('loadStart', this.handleGraphDataSourceStart);
-    this.graphDataSource.removeListener('fetchError', this.handleGraphDataSourceError);
-    this.graphDataSource.removeListener('fetchSuccess', this.handleGraphDataSourceSuccess);
-    this.graphDataSource.removeListener('emptyNamespaces', this.handleGraphDataSourceEmpty);
-  }
-
-  render(): React.ReactNode {
-    let conStyle = containerStyle;
-
-    if (isKioskMode()) {
-      conStyle = kioskContainerStyle;
-    }
-
-    const isEmpty = !(
-      this.state.graphData.elements.nodes && Object.keys(this.state.graphData.elements.nodes).length > 0
-    );
-
-    const isReady = !(isEmpty || this.state.graphData.isError);
-    const isReplayReady = this.props.replayActive && !!this.props.replayQueryTime;
-    const cy = this.cytoscapeGraphRef && this.cytoscapeGraphRef.current ? this.cytoscapeGraphRef.current.getCy() : null;
-
-    return (
-      <>
-        <FlexView className={conStyle} column={true}>
-          <div>
-            <GraphToolbar
-              cy={cy}
-              disabled={this.state.graphData.isLoading}
-              elementsChanged={this.state.graphData.elementsChanged}
-              onToggleHelp={this.toggleHelp}
-            />
-          </div>
-          <FlexView
-            grow={true}
-            className={`${cytoscapeGraphWrapperDivStyle} ${this.props.replayActive && replayBorder}`}
-          >
-            <ErrorBoundary
-              ref={this.errorBoundaryRef}
-              onError={this.notifyError}
-              fallBackComponent={<GraphErrorBoundaryFallback />}
-            >
-              {this.props.showLegend && (
-                <GraphLegend className={graphLegendStyle} closeLegend={this.props.toggleLegend} />
-              )}
-              {isReady && (
-                <Chip
-                  className={`${graphTimeRange} ${this.props.replayActive ? replayBackground : graphBackground}`}
-                  isReadOnly={true}
-                >
-                  {this.props.replayActive && <Badge style={{ marginRight: '4px' }} isRead={true}>{`Replay`}</Badge>}
-                  {!isReplayReady && this.props.replayActive && `click Play to start`}
-                  {!isReplayReady && !this.props.replayActive && `${this.displayTimeRange()}`}
-                  {isReplayReady && `${this.displayTimeRange()}`}
-                </Chip>
-              )}
-              {(!this.props.replayActive || isReplayReady) && (
-                <CytoscapeGraph
-                  containerClassName={cytoscapeGraphContainerStyle}
-                  contextMenuEdgeComponent={EdgeContextMenu}
-                  contextMenuNodeComponent={NodeContextMenu}
-                  focusSelector={this.focusSelector}
-                  graphData={this.state.graphData}
-                  onEmptyGraphAction={this.handleEmptyGraphAction}
-                  onDeleteTrafficRouting={this.handleDeleteTrafficRouting}
-                  onLaunchWizard={this.handleLaunchWizard}
-                  onNodeDoubleTap={this.handleDoubleTap}
-                  ref={refInstance => this.setCytoscapeGraph(refInstance)}
-                  {...this.props}
-                />
-              )}
-              {isReady && (
-                <div
-                  className={cytoscapeToolbarWrapperDivStyle}
-                  style={{ left: this.props.showLegend ? '225px' : '0' }}
-                >
-                  <CytoscapeToolbar
-                    cytoscapeGraphRef={this.cytoscapeGraphRef}
-                    disabled={this.state.graphData.isLoading}
-                  />
-                </div>
-              )}
-            </ErrorBoundary>
-
-            {isReady && this.props.summaryData && (
-              <SummaryPanel
-                data={this.props.summaryData}
-                duration={this.state.graphData.fetchParams.duration}
-                graphType={this.props.graphType}
-                injectServiceNodes={this.props.showServiceNodes}
-                isPageVisible={this.props.isPageVisible}
-                namespaces={this.props.activeNamespaces}
-                onLaunchWizard={this.handleLaunchWizard}
-                onDeleteTrafficRouting={this.handleDeleteTrafficRouting}
-                queryTime={this.state.graphData.timestamp / 1000}
-                trafficRates={this.props.trafficRates}
-                {...computePrometheusRateParams(this.props.duration, NUMBER_OF_DATAPOINTS)}
-              />
-            )}
-          </FlexView>
-        </FlexView>
-
-        <ServiceWizard
-          show={this.state.wizardsData.showWizard}
-          type={this.state.wizardsData.wizardType}
-          update={this.state.wizardsData.updateMode}
-          namespace={this.state.wizardsData.namespace}
-          cluster={this.state.wizardsData.serviceDetails?.service.cluster || ''}
-          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)
-          }
-          virtualServices={this.state.wizardsData.serviceDetails?.virtualServices || []}
-          destinationRules={this.state.wizardsData.serviceDetails?.destinationRules || []}
-          gateways={this.state.wizardsData.gateways || []}
-          k8sGateways={this.state.wizardsData.k8sGateways || []}
-          k8sGRPCRoutes={this.state.wizardsData.serviceDetails?.k8sGRPCRoutes || []}
-          k8sHTTPRoutes={this.state.wizardsData.serviceDetails?.k8sHTTPRoutes || []}
-          peerAuthentications={this.state.wizardsData.peerAuthentications || []}
-          tlsStatus={this.state.wizardsData.serviceDetails?.namespaceMTLS}
-          onClose={this.handleWizardClose}
-          istioAPIEnabled={this.props.istioAPIEnabled}
-        />
-
-        {this.state.showConfirmDeleteTrafficRouting && (
-          <ConfirmDeleteTrafficRoutingModal
-            isOpen={true}
-            destinationRules={DestinationRuleC.fromDrArray(this.state.wizardsData.serviceDetails!.destinationRules)}
-            virtualServices={this.state.wizardsData.serviceDetails!.virtualServices}
-            k8sHTTPRoutes={this.state.wizardsData.serviceDetails!.k8sHTTPRoutes}
-            k8sGRPCRoutes={this.state.wizardsData.serviceDetails!.k8sGRPCRoutes}
-            onCancel={() => this.setState({ showConfirmDeleteTrafficRouting: false })}
-            onConfirm={this.handleConfirmDeleteServiceTrafficRouting}
-          />
-        )}
-      </>
-    );
-  }
-
-  private handleEmptyGraphAction = (): void => {
-    this.loadGraphDataFromBackend();
-  };
-
-  private handleGraphDataSourceSuccess = (
-    graphTimestamp: TimeInSeconds,
-    _,
-    elements: DecoratedGraphElements,
-    fetchParams: FetchParams
-  ): void => {
-    const prevElements = this.state.graphData.elements;
-
-    this.setState({
-      graphData: {
-        elements: elements,
-        elementsChanged: CytoscapeGraphUtils.elementsChanged(prevElements, elements),
-        isLoading: false,
-        fetchParams: fetchParams,
-        timestamp: graphTimestamp * 1000
-      }
-    });
-
-    this.props.setGraphDefinition(this.graphDataSource.graphDefinition);
-  };
-
-  private handleGraphDataSourceError = (errorMessage: string | null, fetchParams: FetchParams): void => {
-    const prevElements = this.state.graphData.elements;
-    this.setState({
-      graphData: {
-        elements: EMPTY_GRAPH_DATA,
-        elementsChanged: CytoscapeGraphUtils.elementsChanged(prevElements, EMPTY_GRAPH_DATA),
-        errorMessage: !!errorMessage ? errorMessage : undefined,
-        isError: true,
-        isLoading: false,
-        fetchParams: fetchParams,
-        timestamp: Date.now()
-      }
-    });
-  };
-
-  private handleGraphDataSourceEmpty = (fetchParams: FetchParams): void => {
-    const prevElements = this.state.graphData.elements;
-    this.setState({
-      graphData: {
-        elements: EMPTY_GRAPH_DATA,
-        elementsChanged: CytoscapeGraphUtils.elementsChanged(prevElements, EMPTY_GRAPH_DATA),
-        isLoading: false,
-        fetchParams: fetchParams,
-        timestamp: Date.now()
-      }
-    });
-  };
-
-  private handleGraphDataSourceStart = (isPreviousDataInvalid: boolean, fetchParams: FetchParams): void => {
-    this.setState({
-      graphData: {
-        elements: isPreviousDataInvalid ? EMPTY_GRAPH_DATA : this.state.graphData.elements,
-        elementsChanged: false,
-        fetchParams: fetchParams,
-        isLoading: true,
-        timestamp: isPreviousDataInvalid ? Date.now() : this.state.graphData.timestamp
-      }
-    });
-  };
-
-  private handleDoubleTap = (event: GraphNodeDoubleTapEvent): void => {
-    if (
-      event.isInaccessible ||
-      event.isServiceEntry ||
-      (event.nodeType === NodeType.BOX && event.isBox !== BoxByType.APP)
-    ) {
-      return;
-    }
-
-    if (event.isOutOfMesh) {
-      AlertUtils.add(
-        `A node with a missing sidecar provides no node-specific telemetry and can not provide a node detail graph.`,
-        undefined,
-        MessageType.WARNING
-      );
-      return;
-    }
-
-    if (event.isIdle) {
-      AlertUtils.add(
-        `An idle node has no node-specific traffic and can not provide a node detail graph.`,
-        undefined,
-        MessageType.WARNING
-      );
-      return;
-    }
-
-    if (event.isOutside && this.props.setActiveNamespaces) {
-      this.props.setActiveNamespaces([{ name: event.namespace }]);
-      return;
-    }
-
-    // If graph is in the drilled-down view, there is the chance that the user
-    // double clicked the same node as in the full graph. Determine if this is
-    // the case.
-    let sameNode = false;
-    const node = this.state.graphData.fetchParams.node;
-
-    if (node) {
-      sameNode = node && node.nodeType === event.nodeType;
-      switch (event.nodeType) {
-        case NodeType.AGGREGATE:
-          sameNode = sameNode && node.aggregate === event.aggregate;
-          sameNode = sameNode && node.aggregateValue === event.aggregateValue;
-          break;
-        case NodeType.APP:
-          sameNode = sameNode && node.app === event.app;
-          sameNode = sameNode && node.version === event.version;
-          break;
-        case NodeType.BOX:
-          // we only support node graphs on app boxes, so assume app box
-          sameNode = sameNode && node.app === event.app;
-          break;
-        case NodeType.SERVICE:
-          sameNode = sameNode && node.service === event.service;
-          break;
-        case NodeType.WORKLOAD:
-          sameNode = sameNode && node.workload === event.workload;
-          break;
-        default:
-          sameNode = true; // don't navigate to unsupported node type
-      }
-    }
-
-    const targetNode: NodeParamsType = { ...event, namespace: { name: event.namespace } };
-
-    // If, while in the drilled-down graph, the user double clicked the same
-    // node as in the main graph, it doesn't make sense to re-load the same view.
-    // Instead, assume that the user wants more details for the node and do a
-    // redirect to the details page.
-    if (sameNode) {
-      this.handleDoubleTapSameNode(targetNode);
-      return;
-    }
-
-    // In case user didn't double-tap the same node, or if graph is in
-    // full graph mode, redirect to the drilled-down graph of the chosen node.
-    const urlParams: GraphUrlParams = {
-      activeNamespaces: this.state.graphData.fetchParams.namespaces,
-      duration: this.state.graphData.fetchParams.duration,
-      edgeLabels: this.state.graphData.fetchParams.edgeLabels,
-      edgeMode: this.props.edgeMode,
-      graphLayout: this.props.layout,
-      graphType: this.state.graphData.fetchParams.graphType,
-      namespaceLayout: this.props.namespaceLayout,
-      node: targetNode,
-      refreshInterval: this.props.refreshInterval,
-      showIdleEdges: this.props.showIdleEdges,
-      showIdleNodes: this.props.showIdleNodes,
-      showOperationNodes: this.props.showOperationNodes,
-      showServiceNodes: this.props.showServiceNodes,
-      showWaypoints: this.props.showWaypoints,
-      trafficRates: this.state.graphData.fetchParams.trafficRates
-    };
-
-    // To ensure updated components get the updated URL, update the URL first and then the state
-    router.navigate(makeNodeGraphUrlFromParams(urlParams));
-  };
-
-  // This allows us to navigate to the service details page when zoomed in on nodes
-  private handleDoubleTapSameNode = (targetNode: NodeParamsType): string | undefined => {
-    const makeAppDetailsPageUrl = (namespace: string, nodeType: string, name?: string): string => {
-      return `/namespaces/${namespace}/${nodeType}/${name}`;
-    };
-
-    const nodeType = targetNode.nodeType;
-    let urlNodeType = `${targetNode.nodeType}s`;
-    let name = targetNode.app;
-
-    if (nodeType === 'service') {
-      name = targetNode.service;
-    } else if (nodeType === 'workload') {
-      name = targetNode.workload;
-    } else {
-      urlNodeType = 'applications';
-    }
-
-    let detailsPageUrl = makeAppDetailsPageUrl(targetNode.namespace.name, urlNodeType, name);
-
-    if (targetNode.cluster && isMultiCluster) {
-      detailsPageUrl = `${detailsPageUrl}?clusterName=${targetNode.cluster}`;
-    }
-
-    if (isParentKiosk(this.props.kiosk)) {
-      kioskContextMenuAction(detailsPageUrl);
-    } else {
-      router.navigate(detailsPageUrl);
-    }
-
-    return;
-  };
-
-  private handleLaunchWizard = (
-    action: WizardAction,
-    mode: WizardMode,
-    namespace: string,
-    serviceDetails: ServiceDetailsInfo,
-    gateways: string[],
-    peerAuths: PeerAuthentication[]
-  ): void => {
-    this.setState(prevState => ({
-      wizardsData: {
-        ...prevState.wizardsData,
-        showWizard: true,
-        wizardType: action,
-        updateMode: mode === 'update',
-        namespace: namespace,
-        serviceDetails: serviceDetails,
-        gateways: gateways,
-        peerAuthentications: peerAuths
-      }
-    }));
-  };
-
-  private handleWizardClose = (changed: boolean): void => {
-    if (changed) {
-      this.setState(prevState => ({
-        wizardsData: {
-          ...prevState.wizardsData,
-          showWizard: false
-        }
-      }));
-
-      triggerRefresh();
-    } else {
-      this.setState(prevState => ({
-        wizardsData: {
-          ...prevState.wizardsData,
-          showWizard: false
-        }
-      }));
-    }
-  };
-
-  private handleDeleteTrafficRouting = (_key: string, serviceDetail: ServiceDetailsInfo): void => {
-    this.setState(prevState => ({
-      showConfirmDeleteTrafficRouting: true,
-      wizardsData: {
-        ...prevState.wizardsData,
-        serviceDetails: serviceDetail
-      }
-    }));
-  };
-
-  private handleConfirmDeleteServiceTrafficRouting = (): void => {
-    this.setState({
-      showConfirmDeleteTrafficRouting: false
-    });
-
-    deleteServiceTrafficRouting(this.state.wizardsData!.serviceDetails!)
-      .then(_results => {
-        AlertUtils.addSuccess(
-          `Istio Config deleted for ${this.state.wizardsData.serviceDetails?.service.name} service.`
-        );
-
-        triggerRefresh();
-      })
-      .catch(error => {
-        AlertUtils.addError('Could not delete Istio config objects.', error);
-      });
-  };
-
-  private toggleHelp = (): void => {
-    if (this.props.showLegend) {
-      this.props.toggleLegend();
-    }
-
-    if (this.props.activeTour) {
-      this.props.endTour();
-    } else {
-      const firstStop = getNextTourStop(GraphTour, -1, 'forward');
-      this.props.startTour({ info: GraphTour, stop: firstStop });
-    }
-  };
-
-  private setCytoscapeGraph(cytoscapeGraph: any): void {
-    this.cytoscapeGraphRef.current = cytoscapeGraph;
-  }
-
-  private loadGraphDataFromBackend = (): void => {
-    const queryTime: TimeInMilliseconds | undefined = !!this.props.replayQueryTime
-      ? this.props.replayQueryTime
-      : undefined;
-
-    this.graphDataSource.fetchGraphData({
-      boxByCluster: this.props.boxByCluster,
-      boxByNamespace: this.props.boxByNamespace,
-      duration: this.props.duration,
-      edgeLabels: this.props.edgeLabels,
-      graphType: this.props.graphType,
-      includeHealth: true,
-      includeLabels: this.props.findValue.includes('label:') || this.props.hideValue.includes('label:'),
-      injectServiceNodes: this.props.showServiceNodes,
-      namespaces: this.props.node ? [this.props.node.namespace] : this.props.activeNamespaces,
-      node: this.props.node,
-      queryTime: queryTime,
-      showIdleEdges: this.props.showIdleEdges,
-      showIdleNodes: this.props.showIdleNodes,
-      showOperationNodes: this.props.showOperationNodes,
-      showSecurity: this.props.showSecurity,
-      showWaypoints: this.props.showWaypoints,
-      trafficRates: this.props.trafficRates
-    });
-  };
-
-  private notifyError = (error: Error, _componentStack: string): void => {
-    AlertUtils.add(`There was an error when rendering the graph: ${error.message}, please try a different layout`);
-  };
-
-  private displayTimeRange = (): string => {
-    const rangeEnd: TimeInMilliseconds = this.state.graphData.timestamp;
-    const rangeStart: TimeInMilliseconds = rangeEnd - this.props.duration * 1000;
-
-    return toRangeString(rangeStart, rangeEnd, { second: '2-digit' }, { second: '2-digit' });
-  };
-}
-
-const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({
-  activeNamespaces: activeNamespacesSelector(state),
-  activeTour: state.tourState.activeTour,
-  boxByCluster: state.graph.toolbarState.boxByCluster,
-  boxByNamespace: state.graph.toolbarState.boxByNamespace,
-  duration: durationSelector(state),
-  edgeLabels: edgeLabelsSelector(state),
-  edgeMode: edgeModeSelector(state),
-  findValue: findValueSelector(state),
-  graphType: graphTypeSelector(state),
-  hideValue: hideValueSelector(state),
-  isPageVisible: state.globalState.isPageVisible,
-  kiosk: state.globalState.kiosk,
-  layout: state.graph.layout,
-  mtlsEnabled: meshWideMTLSEnabledSelector(state),
-  namespaceLayout: state.graph.namespaceLayout,
-  node: state.graph.node,
-  rankBy: state.graph.toolbarState.rankBy,
-  refreshInterval: refreshIntervalSelector(state),
-  replayActive: replayActiveSelector(state),
-  replayQueryTime: replayQueryTimeSelector(state),
-  showIdleEdges: state.graph.toolbarState.showIdleEdges,
-  showIdleNodes: state.graph.toolbarState.showIdleNodes,
-  showLegend: state.graph.toolbarState.showLegend,
-  showOutOfMesh: state.graph.toolbarState.showOutOfMesh,
-  showOperationNodes: state.graph.toolbarState.showOperationNodes,
-  showRank: state.graph.toolbarState.showRank,
-  showSecurity: state.graph.toolbarState.showSecurity,
-  showServiceNodes: state.graph.toolbarState.showServiceNodes,
-  showTrafficAnimation: state.graph.toolbarState.showTrafficAnimation,
-  showVirtualServices: state.graph.toolbarState.showVirtualServices,
-  showWaypoints: state.graph.toolbarState.showWaypoints,
-  summaryData: state.graph.summaryData,
-  trace: state.tracingState?.selectedTrace,
-  trafficRates: trafficRatesSelector(state),
-  istioAPIEnabled: state.statusState.istioEnvironment.istioAPIEnabled,
-  theme: state.globalState.theme
-});
-
-const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => ({
-  endTour: bindActionCreators(TourActions.endTour, dispatch),
-  onNamespaceChange: bindActionCreators(GraphActions.onNamespaceChange, dispatch),
-  onReady: (cy: Cy.Core) => dispatch(GraphThunkActions.graphReady(cy)),
-  setActiveNamespaces: (namespaces: Namespace[]) => dispatch(NamespaceActions.setActiveNamespaces(namespaces)),
-  setGraphDefinition: bindActionCreators(GraphActions.setGraphDefinition, dispatch),
-  setNode: bindActionCreators(GraphActions.setNode, dispatch),
-  setRankResult: bindActionCreators(GraphActions.setRankResult, dispatch),
-  setTraceId: (traceId?: string) => dispatch(TracingThunkActions.setTraceId('', traceId)),
-  setUpdateTime: (val: TimeInMilliseconds) => dispatch(GraphActions.setUpdateTime(val)),
-  startTour: bindActionCreators(TourActions.startTour, dispatch),
-  toggleIdleNodes: bindActionCreators(GraphToolbarActions.toggleIdleNodes, dispatch),
-  toggleLegend: bindActionCreators(GraphToolbarActions.toggleLegend, dispatch),
-  updateSummary: (event: GraphEvent) => dispatch(GraphActions.updateSummary(event))
-});
-
-export const GraphPage = connectRefresh(connect(mapStateToProps, mapDispatchToProps)(GraphPageComponent));
diff --git a/frontend/src/pages/GraphPF/GraphPagePF.tsx b/frontend/src/pages/Graph/GraphPagePF.tsx
similarity index 96%
rename from frontend/src/pages/GraphPF/GraphPagePF.tsx
rename to frontend/src/pages/Graph/GraphPagePF.tsx
index 7c1789e60..b6a35149b 100644
--- a/frontend/src/pages/GraphPF/GraphPagePF.tsx
+++ b/frontend/src/pages/Graph/GraphPagePF.tsx
@@ -11,7 +11,6 @@ import {
   EdgeLabelMode,
   GraphDefinition,
   GraphType,
-  Layout,
   NodeParamsType,
   NodeType,
   SummaryData,
@@ -25,7 +24,7 @@ import { computePrometheusRateParams } from '../../services/Prometheus';
 import * as AlertUtils from '../../utils/AlertUtils';
 import { ErrorBoundary } from '../../components/ErrorBoundary/ErrorBoundary';
 import { GraphToolbar } from '../Graph/GraphToolbar/GraphToolbar';
-import { EmptyGraphLayout } from '../../components/CytoscapeGraph/EmptyGraphLayout';
+import { EmptyGraphLayout } from '../../pages/Graph/EmptyGraphLayout';
 import { SummaryPanel } from '../Graph/SummaryPanel';
 import {
   activeNamespacesSelector,
@@ -68,12 +67,11 @@ import { deleteServiceTrafficRouting } from 'services/Api';
 import { canCreate, canUpdate } from '../../types/Permissions';
 import { connectRefresh } from '../../components/Refresh/connectRefresh';
 import { triggerRefresh } from '../../hooks/refresh';
-import { GraphData } from 'pages/Graph/GraphPage';
-import { GraphPF, FocusNode, getLayoutByName } from './GraphPF';
-import * as CytoscapeGraphUtils from '../../components/CytoscapeGraph/CytoscapeGraphUtils';
+import { GraphPF, FocusNode, getValidGraphLayout, GraphLayout } from './GraphPF';
 import { Controller } from '@patternfly/react-topology';
 import { GraphLegendPF } from './GraphLegendPF';
 import { HistoryManager, URLParam } from 'app/History';
+import { elementsChanged } from 'helpers/GraphHelpers';
 
 // GraphURLPathProps holds path variable values.  Currently all path variables are relevant only to a node graph
 export type GraphURLPathProps = {
@@ -93,7 +91,7 @@ type ReduxDispatchProps = {
   setActiveNamespaces: (namespaces: Namespace[]) => void;
   setEdgeMode: (edgeMode: EdgeMode) => void;
   setGraphDefinition: (graphDefinition: GraphDefinition) => void;
-  setLayout: (layout: Layout) => void;
+  setLayout: (layout: GraphLayout) => void;
   setNode: (node?: NodeParamsType) => void;
   setRankResult: (result: RankResult) => void;
   setTraceId: (traceId?: string) => void;
@@ -117,9 +115,9 @@ type ReduxStateProps = {
   isPageVisible: boolean;
   istioAPIEnabled: boolean;
   kiosk: string;
-  layout: Layout;
+  layout: GraphLayout;
   mtlsEnabled: boolean;
-  namespaceLayout: Layout;
+  namespaceLayout: GraphLayout;
   node?: NodeParamsType;
   rankBy: RankMode[];
   refreshInterval: IntervalInMilliseconds;
@@ -147,6 +145,16 @@ export type GraphPagePropsPF = Partial<GraphURLPathProps> &
     lastRefreshAt: TimeInMilliseconds; // redux by way of ConnectRefresh
   };
 
+export type GraphData = {
+  elements: DecoratedGraphElements;
+  elementsChanged: boolean; // true if current elements differ from previous fetch, can be used as an optimization.
+  errorMessage?: string;
+  fetchParams: FetchParams;
+  isError?: boolean;
+  isLoading: boolean;
+  timestamp: TimeInMilliseconds;
+};
+
 // GraphRefs are passed back from the graph when it is ready, to allow for
 // other components, or test code, to manipulate the graph programatically.
 export type GraphRefs = {
@@ -357,11 +365,13 @@ class GraphPagePFComponent extends React.Component<GraphPagePropsPF, GraphPageSt
     // layout url info
     const urlLayout = HistoryManager.getParam(URLParam.GRAPH_LAYOUT);
     if (urlLayout) {
-      if (urlLayout !== this.props.layout.name) {
-        this.props.setLayout(getLayoutByName(urlLayout));
+      const validLayout = getValidGraphLayout(urlLayout);
+      if (validLayout !== this.props.layout) {
+        this.props.setLayout(validLayout);
+        HistoryManager.setParam(URLParam.GRAPH_LAYOUT, validLayout);
       }
     } else {
-      HistoryManager.setParam(URLParam.GRAPH_LAYOUT, this.props.layout.name);
+      HistoryManager.setParam(URLParam.GRAPH_LAYOUT, this.props.layout);
     }
 
     // Ensure we initialize the graph. We wait for the toolbar to render and
@@ -410,11 +420,7 @@ class GraphPagePFComponent extends React.Component<GraphPagePropsPF, GraphPageSt
       this.loadGraphDataFromBackend();
     }
 
-    if (
-      prev.layout.name !== curr.layout.name ||
-      prev.namespaceLayout.name !== curr.namespaceLayout.name ||
-      activeNamespacesChanged
-    ) {
+    if (prev.layout !== curr.layout || prev.namespaceLayout !== curr.namespaceLayout || activeNamespacesChanged) {
       this.errorBoundaryRef.current.cleanError();
     }
 
@@ -450,7 +456,6 @@ class GraphPagePFComponent extends React.Component<GraphPagePropsPF, GraphPageSt
               controller={this.state.graphRefs?.getController()}
               disabled={this.state.graphData.isLoading}
               elementsChanged={this.state.graphData.elementsChanged}
-              isPF={true}
               onToggleHelp={this.toggleHelp}
             />
           </div>
@@ -575,11 +580,10 @@ class GraphPagePFComponent extends React.Component<GraphPagePropsPF, GraphPageSt
     fetchParams: FetchParams
   ): void => {
     const prevElements = this.state.graphData.elements;
-    const elementsChanged = CytoscapeGraphUtils.elementsChanged(prevElements, elements);
     this.setState({
       graphData: {
         elements: elements,
-        elementsChanged: elementsChanged,
+        elementsChanged: elementsChanged(prevElements, elements),
         isLoading: false,
         fetchParams: fetchParams,
         timestamp: graphTimestamp * 1000
@@ -593,7 +597,7 @@ class GraphPagePFComponent extends React.Component<GraphPagePropsPF, GraphPageSt
     this.setState({
       graphData: {
         elements: EMPTY_GRAPH_DATA,
-        elementsChanged: CytoscapeGraphUtils.elementsChanged(prevElements, EMPTY_GRAPH_DATA),
+        elementsChanged: elementsChanged(prevElements, EMPTY_GRAPH_DATA),
         errorMessage: !!errorMessage ? errorMessage : undefined,
         isError: true,
         isLoading: false,
@@ -608,7 +612,7 @@ class GraphPagePFComponent extends React.Component<GraphPagePropsPF, GraphPageSt
     this.setState({
       graphData: {
         elements: EMPTY_GRAPH_DATA,
-        elementsChanged: CytoscapeGraphUtils.elementsChanged(prevElements, EMPTY_GRAPH_DATA),
+        elementsChanged: elementsChanged(prevElements, EMPTY_GRAPH_DATA),
         isLoading: false,
         fetchParams: fetchParams,
         timestamp: Date.now()
diff --git a/frontend/src/components/CytoscapeGraph/GraphScore.ts b/frontend/src/pages/Graph/GraphScore.ts
similarity index 100%
rename from frontend/src/components/CytoscapeGraph/GraphScore.ts
rename to frontend/src/pages/Graph/GraphScore.ts
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphFind.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphFind.tsx
deleted file mode 100644
index d3b356def..000000000
--- a/frontend/src/pages/Graph/GraphToolbar/GraphFind.tsx
+++ /dev/null
@@ -1,1172 +0,0 @@
-import * as React from 'react';
-import {
-  Button,
-  ButtonVariant,
-  TextInput,
-  Tooltip,
-  Form,
-  FormHelperText,
-  Grid,
-  HelperText,
-  HelperTextItem,
-  FormGroup,
-  GridItem
-} from '@patternfly/react-core';
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import { KialiAppState } from '../../../store/Store';
-import { findValueSelector, hideValueSelector, edgeLabelsSelector, edgeModeSelector } from '../../../store/Selectors';
-import { GraphToolbarActions } from '../../../actions/GraphToolbarActions';
-import { GraphHelpFind } from '../../../pages/Graph/GraphHelpFind';
-import * as CytoscapeGraphUtils from '../../../components/CytoscapeGraph/CytoscapeGraphUtils';
-import { EdgeLabelMode, NodeType, Layout, EdgeMode, EdgeAttr, NodeAttr } from '../../../types/Graph';
-import * as AlertUtils from '../../../utils/AlertUtils';
-import { KialiIcon } from 'config/KialiIcon';
-import { kialiStyle } from 'styles/StyleUtils';
-import { TourStop } from 'components/Tour/TourStop';
-import { GraphTourStops } from 'pages/Graph/GraphHelpTour';
-import { TimeInMilliseconds } from 'types/Common';
-import { KialiDispatch } from 'types/Redux';
-import { AutoComplete } from 'utils/AutoComplete';
-import { HEALTHY, NA, NOT_READY } from 'types/Health';
-import { GraphFindOptions } from './GraphFindOptions';
-import { location, HistoryManager, URLParam } from '../../../app/History';
-import { isValid } from 'utils/Common';
-import { serverConfig } from '../../../config';
-import { infoStyle } from 'styles/IconStyle';
-
-type ReduxStateProps = {
-  edgeLabels: EdgeLabelMode[];
-  edgeMode: EdgeMode;
-  findValue: string;
-  hideValue: string;
-  layout: Layout;
-  namespaceLayout: Layout;
-  showFindHelp: boolean;
-  showIdleNodes: boolean;
-  showRank: boolean;
-  showSecurity: boolean;
-  updateTime: TimeInMilliseconds;
-};
-
-type ReduxDispatchProps = {
-  setEdgeLabels: (vals: EdgeLabelMode[]) => void;
-  setFindValue: (val: string) => void;
-  setHideValue: (val: string) => void;
-  toggleFindHelp: () => void;
-  toggleGraphSecurity: () => void;
-  toggleIdleNodes: () => void;
-  toggleRank: () => void;
-};
-
-type GraphFindProps = ReduxStateProps &
-  ReduxDispatchProps & {
-    cy: any;
-    elementsChanged: boolean;
-  };
-
-type GraphFindState = {
-  findError?: string;
-  findInputValue: string;
-  hideError?: string;
-  hideInputValue: string;
-};
-
-type ParsedExpression = {
-  selector: string;
-  target: 'node' | 'edge';
-};
-
-const thinGroupStyle = kialiStyle({
-  paddingLeft: '0.75rem'
-});
-
-const buttonClearStyle = kialiStyle({
-  paddingLeft: '0.75rem',
-  paddingRight: '0.75rem'
-});
-
-const findHideHelpStyle = kialiStyle({
-  paddingLeft: '0',
-  paddingRight: '0'
-});
-
-const gridStyle = kialiStyle({
-  display: 'flex'
-});
-
-const graphFindStyle = kialiStyle({
-  marginRight: '0.75rem',
-  $nest: {
-    '& > .pf-v5-c-form__group-control': {
-      display: 'flex'
-    }
-  }
-});
-
-const graphHideStyle = kialiStyle({
-  $nest: {
-    '& > .pf-v5-c-form__group-control': {
-      display: 'flex'
-    }
-  }
-});
-
-const operands: string[] = [
-  '%grpcerr',
-  '%grpctraffic',
-  '%httperr',
-  '%httptraffic',
-  'app',
-  'circuitbreaker',
-  'cluster',
-  'destprincipal',
-  'faultinjection',
-  'grpc',
-  'grpcerr',
-  'grpcin',
-  'grpcout',
-  'healthy',
-  'http',
-  'httpin',
-  'httpout',
-  'idle',
-  'label:',
-  'mirroring',
-  'mtls',
-  'name',
-  'namespace',
-  'node',
-  'operation',
-  'outside',
-  'protocol',
-  'rank',
-  'requestrouting',
-  'requesttimeout',
-  'responsetime',
-  'service',
-  'serviceentry',
-  'sidecar',
-  'sourceprincipal',
-  'tcp',
-  'tcptrafficshifting',
-  'throughput',
-  'traffic',
-  'trafficshifting',
-  'trafficsource',
-  'version',
-  'virtualservice',
-  'tcpin',
-  'tcpout',
-  'workload',
-  'workloadentry'
-];
-
-export class GraphFindComponent extends React.Component<GraphFindProps, GraphFindState> {
-  static contextTypes = {
-    router: (): null => null
-  };
-
-  private findAutoComplete: AutoComplete;
-  private findInputRef;
-  private hiddenElements: any | undefined;
-  private hideAutoComplete: AutoComplete;
-  private hideInputRef;
-  private removedElements: any | undefined;
-
-  constructor(props: GraphFindProps) {
-    super(props);
-
-    this.findAutoComplete = new AutoComplete(operands);
-    this.hideAutoComplete = new AutoComplete(operands);
-
-    let findValue = props.findValue ? props.findValue : '';
-    let hideValue = props.hideValue ? props.hideValue : '';
-
-    // Let URL override current redux state at construction time. Update URL as needed.
-    const urlParams = new URLSearchParams(location.getSearch());
-    const urlFind = HistoryManager.getParam(URLParam.GRAPH_FIND, urlParams);
-
-    if (!!urlFind) {
-      if (urlFind !== findValue) {
-        findValue = urlFind;
-        props.setFindValue(urlFind);
-      }
-    } else if (!!findValue) {
-      HistoryManager.setParam(URLParam.GRAPH_FIND, findValue);
-    } else {
-      const autoSelect = serverConfig.kialiFeatureFlags.uiDefaults.graph.findOptions.find(opt => opt.autoSelect);
-
-      if (autoSelect) {
-        props.setFindValue(autoSelect.expression);
-      }
-    }
-
-    const urlHide = HistoryManager.getParam(URLParam.GRAPH_HIDE, urlParams);
-
-    if (!!urlHide) {
-      if (urlHide !== hideValue) {
-        hideValue = urlHide;
-        props.setHideValue(urlHide);
-      }
-    } else if (!!hideValue) {
-      HistoryManager.setParam(URLParam.GRAPH_HIDE, hideValue);
-    } else {
-      const autoSelect = serverConfig.kialiFeatureFlags.uiDefaults.graph.hideOptions.find(opt => opt.autoSelect);
-
-      if (autoSelect) {
-        props.setHideValue(autoSelect.expression);
-      }
-    }
-
-    this.state = { findInputValue: findValue, hideInputValue: hideValue };
-
-    if (props.showFindHelp) {
-      props.toggleFindHelp();
-    }
-  }
-
-  // We only update on a change to the find/hide values, or a graph change.  Although we use other props
-  // in processing (layout, etc), a change to those settings will generate a graph change, so we
-  // wait for the graph change to do the update.
-  shouldComponentUpdate(nextProps: GraphFindProps, nextState: GraphFindState): boolean {
-    const cyChanged = this.props.cy !== nextProps.cy;
-    const edgeModeChanged = this.props.edgeMode !== nextProps.edgeMode;
-    const findChanged = this.props.findValue !== nextProps.findValue;
-    const hideChanged = this.props.hideValue !== nextProps.hideValue;
-    const graphChanged = this.props.updateTime !== nextProps.updateTime;
-    const showFindHelpChanged = this.props.showFindHelp !== nextProps.showFindHelp;
-    const findErrorChanged = this.state.findError !== nextState.findError;
-    const hideErrorChanged = this.state.hideError !== nextState.hideError;
-
-    const shouldUpdate =
-      cyChanged ||
-      edgeModeChanged ||
-      findChanged ||
-      hideChanged ||
-      graphChanged ||
-      showFindHelpChanged ||
-      findErrorChanged ||
-      hideErrorChanged;
-
-    return shouldUpdate;
-  }
-
-  // Note that we may have redux hide/find values set at mount-time. But because the toolbar mounts prior to
-  // the graph loading, we can't perform this graph "post-processing" until we have a valid cy graph.  But the
-  // find/hide processing will be initiated externally (CytoscapeGraph:processgraphUpdate) when the graph is ready.
-  componentDidUpdate(prevProps: GraphFindProps): void {
-    if (!this.props.cy) {
-      this.hiddenElements = undefined;
-      this.removedElements = undefined;
-      return;
-    }
-
-    const edgeModeChanged = this.props.edgeMode !== prevProps.edgeMode;
-    const findChanged = this.props.findValue !== prevProps.findValue;
-    const hideChanged = this.props.hideValue !== prevProps.hideValue;
-    const graphChanged = this.props.updateTime !== prevProps.updateTime;
-    const graphElementsChanged = graphChanged && this.props.elementsChanged;
-
-    // ensure redux state and URL are aligned
-    if (findChanged) {
-      if (!this.props.findValue) {
-        HistoryManager.deleteParam(URLParam.GRAPH_FIND);
-      } else {
-        HistoryManager.setParam(URLParam.GRAPH_FIND, this.props.findValue);
-      }
-    }
-
-    if (hideChanged) {
-      if (!this.props.hideValue) {
-        HistoryManager.deleteParam(URLParam.GRAPH_HIDE);
-      } else {
-        HistoryManager.setParam(URLParam.GRAPH_HIDE, this.props.hideValue);
-      }
-    }
-
-    // make sure the value is updated if there was a change
-    if (findChanged || (graphChanged && !!this.props.findValue)) {
-      // ensure findInputValue is aligned if findValue is set externally (e.g. resetSettings)
-      if (this.state.findInputValue !== this.props.findValue) {
-        this.setFind(this.props.findValue);
-      }
-
-      this.handleFind(this.props.cy);
-    }
-
-    if (
-      hideChanged ||
-      (graphChanged && !!this.props.hideValue) ||
-      edgeModeChanged ||
-      this.props.edgeMode !== EdgeMode.ALL
-    ) {
-      // ensure hideInputValue is aligned if hideValue is set externally (e.g. resetSettings)
-      if (this.state.hideInputValue !== this.props.hideValue) {
-        this.setHide(this.props.hideValue);
-      }
-
-      this.handleHide(this.props.cy, hideChanged, graphChanged, graphElementsChanged, edgeModeChanged);
-    }
-  }
-
-  render(): React.ReactNode {
-    return (
-      <TourStop info={GraphTourStops.Find}>
-        <Form className={thinGroupStyle}>
-          <Grid md={12} className={gridStyle}>
-            <GridItem span={5}>
-              <FormGroup>
-                <TextInput
-                  id="graph_find"
-                  name="graph_find"
-                  ref={ref => {
-                    this.findInputRef = ref;
-                  }}
-                  type="text"
-                  autoComplete="on"
-                  validated={isValid(this.state.findInputValue ? !this.state.findError : undefined)}
-                  onChange={(_event, val) => this.updateFind(val)}
-                  defaultValue={this.state.findInputValue}
-                  onKeyDownCapture={this.checkSpecialKeyFind}
-                  placeholder="Find..."
-                />
-                {this.state.findError && (
-                  <FormHelperText>
-                    <HelperText>
-                      <HelperTextItem variant={'error'}>{this.state.findError}</HelperTextItem>
-                    </HelperText>
-                  </FormHelperText>
-                )}
-              </FormGroup>
-            </GridItem>
-
-            <GridItem span={1}>
-              <FormGroup className={graphFindStyle}>
-                <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('')}
-                    >
-                      <KialiIcon.Close />
-                    </Button>
-                  </Tooltip>
-                )}
-              </FormGroup>
-            </GridItem>
-
-            <GridItem span={5}>
-              <FormGroup>
-                <TextInput
-                  id="graph_hide"
-                  name="graph_hide"
-                  ref={ref => {
-                    this.hideInputRef = ref;
-                  }}
-                  autoComplete="on"
-                  validated={isValid(this.state.hideInputValue ? !this.state.hideError : undefined)}
-                  type="text"
-                  onChange={(_event, val) => this.updateHide(val)}
-                  defaultValue={this.state.hideInputValue}
-                  onKeyDownCapture={this.checkSpecialKeyHide}
-                  placeholder="Hide..."
-                />
-                {this.state.hideError && (
-                  <FormHelperText>
-                    <HelperText>
-                      <HelperTextItem variant={'error'}>{this.state.hideError}</HelperTextItem>
-                    </HelperText>
-                  </FormHelperText>
-                )}
-              </FormGroup>
-            </GridItem>
-
-            <GridItem span={1}>
-              <FormGroup className={graphHideStyle}>
-                <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('')}
-                    >
-                      <KialiIcon.Close />
-                    </Button>
-                  </Tooltip>
-                )}
-              </FormGroup>
-            </GridItem>
-          </Grid>
-        </Form>
-
-        {this.props.showFindHelp ? (
-          <GraphHelpFind onClose={this.toggleFindHelp}>
-            <Button
-              data-test="graph-find-hide-help-button"
-              variant={ButtonVariant.link}
-              className={findHideHelpStyle}
-              onClick={this.toggleFindHelp}
-            >
-              <KialiIcon.Info className={infoStyle} />
-            </Button>
-          </GraphHelpFind>
-        ) : (
-          <Tooltip key={'ot_graph_find_help'} position="top" content="Click to open Find/Hide help">
-            <Button
-              data-test="graph-find-hide-help-button"
-              variant={ButtonVariant.link}
-              className={findHideHelpStyle}
-              onClick={this.toggleFindHelp}
-            >
-              <KialiIcon.Info className={infoStyle} />
-            </Button>
-          </Tooltip>
-        )}
-      </TourStop>
-    );
-  }
-
-  private toggleFindHelp = (): void => {
-    this.props.toggleFindHelp();
-  };
-
-  private checkSpecialKeyFind = (event: React.KeyboardEvent): void => {
-    const keyCode = event.keyCode ? event.keyCode : event.which;
-    switch (keyCode) {
-      case 9: // tab (autocomplete)
-        event.preventDefault();
-        const next = this.findAutoComplete.next();
-
-        if (!!next) {
-          this.findInputRef.value = next;
-          this.findInputRef.scrollLeft = this.findInputRef.scrollWidth;
-          this.setState({ findInputValue: next, findError: undefined });
-        }
-
-        break;
-      case 13: // return (submit)
-        event.preventDefault();
-        this.submitFind();
-        break;
-      default:
-        break;
-    }
-  };
-
-  private updateFindOption = (key: string): void => {
-    this.setFind(key);
-  };
-
-  private updateFind = (val: string): void => {
-    if ('' === val) {
-      this.setFind('');
-    } else {
-      const diff = Math.abs(val.length - this.state.findInputValue.length);
-      this.findAutoComplete.setInput(val, [' ', '!']);
-      this.setState({ findInputValue: val, findError: undefined });
-      // submit if length change is greater than a single key, assume browser suggestion clicked or user paste
-      if (diff > 1) {
-        this.props.setFindValue(val);
-      }
-    }
-  };
-
-  private setFind = (val: string): void => {
-    // TODO: when TextInput refs are fixed in PF4 then use the ref and remove the direct HTMLElement usage
-    this.findInputRef.value = val;
-    const htmlInputElement: HTMLInputElement = document.getElementById('graph_find') as HTMLInputElement;
-
-    if (htmlInputElement !== null) {
-      htmlInputElement.value = val;
-    }
-
-    this.findAutoComplete.setInput(val);
-    this.setState({ findInputValue: val, findError: undefined });
-    this.props.setFindValue(val);
-  };
-
-  private submitFind = (): void => {
-    if (this.props.findValue !== this.state.findInputValue) {
-      this.props.setFindValue(this.state.findInputValue);
-    }
-  };
-
-  private checkSpecialKeyHide = (event: React.KeyboardEvent): void => {
-    const keyCode = event.keyCode ? event.keyCode : event.which;
-    switch (keyCode) {
-      case 9: // tab (autocomplete)
-        event.preventDefault();
-        const next = this.hideAutoComplete.next();
-
-        if (!!next) {
-          this.hideInputRef.value = next;
-          this.hideInputRef.scrollLeft = this.hideInputRef.scrollWidth;
-          this.setState({ hideInputValue: next, hideError: undefined });
-        }
-
-        break;
-      case 13: // return (submit)
-        event.preventDefault();
-        this.submitHide();
-        break;
-      default:
-        break;
-    }
-  };
-
-  private updateHideOption = (key: string): void => {
-    this.setHide(key);
-  };
-
-  private updateHide = (val: string): void => {
-    if ('' === val) {
-      this.setHide('');
-    } else {
-      const diff = Math.abs(val.length - this.state.hideInputValue.length);
-      this.hideAutoComplete.setInput(val, [' ', '!']);
-      this.setState({ hideInputValue: val, hideError: undefined });
-
-      // submit if length change is greater than a single key, assume browser suggestion clicked or user paste
-      if (diff > 1) {
-        this.props.setHideValue(val);
-      }
-    }
-  };
-
-  private submitHide = (): void => {
-    if (this.props.hideValue !== this.state.hideInputValue) {
-      this.props.setHideValue(this.state.hideInputValue);
-    }
-  };
-
-  private setHide = (val: string): void => {
-    // TODO: when TextInput refs are fixed in PF4 then use the ref and remove the direct HTMLElement usage
-    this.hideInputRef.value = val;
-    const htmlInputElement: HTMLInputElement = document.getElementById('graph_hide') as HTMLInputElement;
-
-    if (htmlInputElement !== null) {
-      htmlInputElement.value = val;
-    }
-
-    this.hideAutoComplete.setInput(val);
-    this.setState({ hideInputValue: val, hideError: undefined });
-    this.props.setHideValue(val);
-  };
-
-  private handleHide = (
-    cy: any,
-    hideChanged: boolean,
-    graphChanged: boolean,
-    graphElementsChanged: boolean,
-    edgeModeChanged: boolean
-  ): void => {
-    const selector = this.parseValue(this.props.hideValue, false);
-    const checkRemovals = selector || this.props.edgeMode !== EdgeMode.ALL;
-
-    console.debug(`Hide selector=[${selector}]`);
-
-    cy.startBatch();
-
-    // unhide hidden elements when we are dealing with the same graph. Either way,release for garbage collection
-    if (!!this.hiddenElements && !graphChanged) {
-      this.hiddenElements.style({ visibility: 'visible' });
-    }
-    this.hiddenElements = undefined;
-
-    // restore removed elements when we are working with the same graph. Either way,release for garbage collection.
-    if (!!this.removedElements && !graphChanged) {
-      this.removedElements.restore();
-    }
-    this.removedElements = undefined;
-
-    // select the new hide-hits
-    if (checkRemovals) {
-      let hiddenElements = cy.collection();
-
-      if (selector) {
-        // add elements described by the hide expression
-        hiddenElements = cy.$(selector);
-
-        // add nodes with only hidden edges (keep idle nodes as that is an explicit option)
-        const visibleElements = hiddenElements.absoluteComplement();
-        const nodesWithVisibleEdges = visibleElements.edges().connectedNodes();
-        const nodesWithOnlyHiddenEdges = visibleElements.nodes(`[^${NodeAttr.isIdle}]`).subtract(nodesWithVisibleEdges);
-        hiddenElements = hiddenElements.add(nodesWithOnlyHiddenEdges);
-
-        // add the edges connected to hidden nodes
-        hiddenElements = hiddenElements.add(hiddenElements.connectedEdges());
-
-        // subtract any appbox hits, we only hide empty appboxes
-        hiddenElements = hiddenElements.subtract(hiddenElements.filter('$node[isBox]'));
-      }
-
-      if (this.props.edgeMode !== EdgeMode.ALL) {
-        // remove other unwanted edges, don't touch the remaining nodes
-        const visibleElements = hiddenElements.absoluteComplement();
-        switch (this.props.edgeMode) {
-          case EdgeMode.NONE:
-            hiddenElements = hiddenElements.add(visibleElements.edges());
-            break;
-          case EdgeMode.UNHEALTHY:
-            hiddenElements = hiddenElements.add(
-              visibleElements.edges(`[^${EdgeAttr.healthStatus}],[${EdgeAttr.healthStatus} = "${HEALTHY.name}"]`)
-            );
-            break;
-        }
-      }
-
-      this.removedElements = cy.remove(hiddenElements);
-      // now subtract any boxes that no longer have children. this is iterative as boxes may be nested
-      let done = false;
-
-      while (!done) {
-        const emptyBoxes = cy.$('$node[isBox]').subtract(cy.$('$node[isBox] > :inside'));
-
-        if (emptyBoxes.length > 0) {
-          this.removedElements = this.removedElements.add(cy.remove(emptyBoxes));
-        } else {
-          done = true;
-        }
-      }
-    }
-
-    cy.endBatch();
-
-    const hasRemovedElements: boolean = !!this.removedElements && this.removedElements.length > 0;
-
-    if (hideChanged || (hasRemovedElements && graphElementsChanged) || edgeModeChanged) {
-      cy.emit('kiali-zoomignore', [true]);
-      CytoscapeGraphUtils.runLayout(cy, this.props.layout, this.props.namespaceLayout).then(() => {
-        // do nothing, defer to CytoscapeGraph.tsx 'onlayout' event handler
-      });
-    }
-  };
-
-  private handleFind = (cy: any): void => {
-    const selector = this.parseValue(this.props.findValue, true);
-    console.debug(`Find selector=[${selector}]`);
-
-    cy.startBatch();
-    // unhighlight old find-hits
-    cy.elements('*.find').removeClass('find');
-
-    if (selector) {
-      // add new find-hits
-      cy.elements(selector).addClass('find');
-    }
-
-    cy.endBatch();
-  };
-
-  private setError(error: string | undefined, isFind: boolean): undefined {
-    if (isFind && error !== this.state.findError) {
-      const findError = !!error ? `Find: ${error}` : undefined;
-      this.setState({ findError: findError });
-    } else if (error !== this.state.hideError) {
-      const hideError = !!error ? `Hide: ${error}` : undefined;
-      this.setState({ hideError: hideError });
-    }
-
-    return undefined;
-  }
-
-  private parseValue = (val: string, isFind: boolean): string | undefined => {
-    let preparedVal = this.prepareValue(val);
-    if (!preparedVal) {
-      return undefined;
-    }
-
-    // generate separate selectors for each disjunctive clause and then stitch them together. This
-    // lets us mix node and edge criteria.
-    const orClauses = preparedVal.split(' OR ');
-    let orSelector;
-
-    for (const clause of orClauses) {
-      const expressions = clause.split(' AND ');
-      const conjunctive = expressions.length > 1;
-      let selector;
-
-      for (const expression of expressions) {
-        const parsedExpression = this.parseExpression(expression, conjunctive, isFind);
-        if (!parsedExpression) {
-          return undefined;
-        }
-
-        selector = this.appendSelector(selector, parsedExpression, isFind);
-        if (!selector) {
-          return undefined;
-        }
-      }
-      // parsed successfully, clear any previous error message
-      this.setError(undefined, isFind);
-      orSelector = !orSelector ? selector : `${orSelector},${selector}`;
-    }
-
-    return orSelector;
-  };
-
-  private prepareValue = (val: string): string => {
-    // remove double spaces
-    val = val.replace(/ +(?= )/g, '');
-
-    // remove unnecessary mnemonic qualifiers on unary operators (e.g. 'has cb' -> 'cb').
-    val = ` ${val}`;
-    val = val.replace(/ is /gi, ' ');
-    val = val.replace(/ has /gi, ' ');
-    val = val.replace(/ !\s*is /gi, ' ! ');
-    val = val.replace(/ !\s*has /gi, ' ! ');
-
-    // replace string operators
-    val = val.replace(/ not /gi, ' !');
-    val = val.replace(/ !\s*contains /gi, ' !*= ');
-    val = val.replace(/ !\s*startswith /gi, ' !^= ');
-    val = val.replace(/ !\s*endswith /gi, ' !$= ');
-    val = val.replace(/ contains /gi, ' *= ');
-    val = val.replace(/ startswith /gi, ' ^= ');
-    val = val.replace(/ endswith /gi, ' $= ');
-
-    // uppercase conjunctions
-    val = val.replace(/ and /gi, ' AND ');
-    val = val.replace(/ or /gi, ' OR ');
-
-    return val.trim();
-  };
-
-  private parseExpression = (
-    expression: string,
-    conjunctive: boolean,
-    isFind: boolean
-  ): ParsedExpression | undefined => {
-    let op;
-
-    if (expression.includes('!=')) {
-      op = '!=';
-    } else if (expression.includes('!*=')) {
-      op = '!*=';
-    } else if (expression.includes('!$=')) {
-      op = '!$=';
-    } else if (expression.includes('!^=')) {
-      op = '!^=';
-    } else if (expression.includes('>=')) {
-      op = '>=';
-    } else if (expression.includes('<=')) {
-      op = '<=';
-    } else if (expression.includes('*=')) {
-      op = '*='; // substring
-    } else if (expression.includes('$=')) {
-      op = '$='; // starts with
-    } else if (expression.includes('^=')) {
-      op = '^='; // ends with
-    } else if (expression.includes('=')) {
-      op = '=';
-    } else if (expression.includes('>')) {
-      op = '>';
-    } else if (expression.includes('<')) {
-      op = '<';
-    } else if (expression.includes('!')) {
-      op = '!';
-    }
-
-    if (!op) {
-      if (expression.split(' ').length > 1) {
-        return this.setError(`No valid operator found in expression`, isFind);
-      }
-
-      const unaryExpression = this.parseUnaryFindExpression(expression.trim(), false);
-      return unaryExpression ? unaryExpression : this.setError(`Invalid Node or Edge operand`, isFind);
-    }
-
-    const tokens = expression.split(op);
-    if (op === '!') {
-      const unaryExpression = this.parseUnaryFindExpression(tokens[1].trim(), true);
-      return unaryExpression ? unaryExpression : this.setError(`Invalid Node or Edge operand`, isFind);
-    }
-
-    const field = tokens[0].trim();
-    const val = tokens[1].trim();
-
-    switch (field.toLowerCase()) {
-      //
-      // nodes...
-      //
-      case 'app':
-        return { target: 'node', selector: `[${NodeAttr.app} ${op} "${val}"]` };
-      case 'cluster':
-        return { target: 'node', selector: `[${NodeAttr.cluster} ${op} "${val}"]` };
-      case 'grpcin': {
-        const s = this.getNumericSelector(NodeAttr.grpcIn, op, val, expression, isFind);
-        return s ? { target: 'node', selector: s } : undefined;
-      }
-      case 'grpcout': {
-        const s = this.getNumericSelector(NodeAttr.grpcOut, op, val, expression, isFind);
-        return s ? { target: 'node', selector: s } : undefined;
-      }
-      case 'httpin': {
-        const s = this.getNumericSelector(NodeAttr.httpIn, op, val, expression, isFind);
-        return s ? { target: 'node', selector: s } : undefined;
-      }
-      case 'httpout': {
-        const s = this.getNumericSelector(NodeAttr.httpOut, op, val, expression, isFind);
-        return s ? { target: 'node', selector: s } : undefined;
-      }
-      case 'name': {
-        const isNegation = op.startsWith('!');
-
-        if (conjunctive) {
-          return this.setError(`Can not use 'AND' with 'name' operand`, isFind);
-        }
-
-        const agg = `[${NodeAttr.aggregateValue} ${op} "${val}"]`;
-        const app = `[${NodeAttr.app} ${op} "${val}"]`;
-        const svc = `[${NodeAttr.service} ${op} "${val}"]`;
-        const wl = `[${NodeAttr.workload} ${op} "${val}"]`;
-
-        return { target: 'node', selector: isNegation ? `${agg}${app}${svc}${wl}` : `${agg},${app},${svc},${wl}` };
-      }
-      case 'node':
-        let nodeType = val.toLowerCase();
-        switch (nodeType) {
-          case 'op':
-          case 'operation':
-            nodeType = NodeType.AGGREGATE;
-            break;
-          case 'svc':
-            nodeType = NodeType.SERVICE;
-            break;
-          case 'wl':
-            nodeType = NodeType.WORKLOAD;
-            break;
-          default:
-            break; // no-op
-        }
-
-        switch (nodeType) {
-          case NodeType.AGGREGATE:
-          case NodeType.APP:
-          case NodeType.SERVICE:
-          case NodeType.WORKLOAD:
-          case NodeType.UNKNOWN:
-            return { target: 'node', selector: `[${NodeAttr.nodeType} ${op} "${nodeType}"]` };
-          default:
-            this.setError(
-              `Invalid node type [${nodeType}]. Expected app | operation | service | unknown | workload`,
-              isFind
-            );
-        }
-
-        return undefined;
-      case 'ns':
-      case 'namespace':
-        return { target: 'node', selector: `[${NodeAttr.namespace} ${op} "${val}"]` };
-      case 'op':
-      case 'operation':
-        return { target: 'node', selector: `[${NodeAttr.aggregateValue} ${op} "${val}"]` };
-      case 'rank': {
-        if (!this.props.showRank) {
-          AlertUtils.addSuccess('Enabling "Rank" display option for graph find/hide expression');
-          this.props.toggleRank();
-        }
-
-        const valAsNum = Number(val);
-        if (Number.isNaN(valAsNum) || valAsNum < 1 || valAsNum > 100) {
-          return this.setError(`Invalid rank range [${val}]. Expected a number between 1..100`, isFind);
-        }
-
-        const s = this.getNumericSelector(NodeAttr.rank, op, val, expression, isFind);
-        return s ? { target: 'node', selector: s } : undefined;
-      }
-      case 'svc':
-      case 'service':
-        return { target: 'node', selector: `[${NodeAttr.service} ${op} "${val}"]` };
-      case 'tcpin': {
-        const s = this.getNumericSelector(NodeAttr.tcpIn, op, val, expression, isFind);
-        return s ? { target: 'node', selector: s } : undefined;
-      }
-      case 'tcpout': {
-        const s = this.getNumericSelector(NodeAttr.tcpOut, op, val, expression, isFind);
-        return s ? { target: 'node', selector: s } : undefined;
-      }
-      case 'version':
-        return { target: 'node', selector: `[${NodeAttr.version} ${op} "${val}"]` };
-      case 'wl':
-      case 'workload':
-        return { target: 'node', selector: `[${NodeAttr.workload} ${op} "${val}"]` };
-      //
-      // edges..
-      //
-      case 'destprincipal':
-        if (!this.props.showSecurity) {
-          AlertUtils.addSuccess('Enabling "Security" display option for graph find/hide expression');
-          this.props.toggleGraphSecurity();
-        }
-        return { target: 'edge', selector: `[${EdgeAttr.destPrincipal} ${op} "${val}"]` };
-      case 'grpc': {
-        const s = this.getNumericSelector(EdgeAttr.grpc, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case '%grpcerror':
-      case '%grpcerr': {
-        const s = this.getNumericSelector(EdgeAttr.grpcPercentErr, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case '%grpctraffic': {
-        const s = this.getNumericSelector(EdgeAttr.grpcPercentReq, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case 'http': {
-        const s = this.getNumericSelector(EdgeAttr.http, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case '%httperror':
-      case '%httperr': {
-        const s = this.getNumericSelector(EdgeAttr.httpPercentErr, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case '%httptraffic': {
-        const s = this.getNumericSelector(EdgeAttr.httpPercentReq, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case 'protocol': {
-        return { target: 'edge', selector: `[${EdgeAttr.protocol} ${op} "${val}"]` };
-      }
-      case 'rt':
-      case 'responsetime': {
-        if (!this.props.edgeLabels.includes(EdgeLabelMode.RESPONSE_TIME_GROUP)) {
-          AlertUtils.addSuccess('Enabling [P95] "Response Time" edge labels for this graph find/hide expression');
-          this.props.setEdgeLabels([
-            ...this.props.edgeLabels,
-            EdgeLabelMode.RESPONSE_TIME_GROUP,
-            EdgeLabelMode.RESPONSE_TIME_P95
-          ]);
-        }
-
-        const s = this.getNumericSelector(EdgeAttr.responseTime, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case 'sourceprincipal':
-        if (!this.props.showSecurity) {
-          AlertUtils.addSuccess('Enabling "Security" display option for this graph find/hide expression');
-          this.props.toggleGraphSecurity();
-        }
-
-        return { target: 'edge', selector: `[${EdgeAttr.sourcePrincipal} ${op} "${val}"]` };
-      case 'tcp': {
-        const s = this.getNumericSelector(EdgeAttr.tcp, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      case 'throughput': {
-        if (!this.props.edgeLabels.includes(EdgeLabelMode.THROUGHPUT_GROUP)) {
-          AlertUtils.addSuccess('Enabling [Request] "Throughput" edge labels for this graph find/hide expression');
-          this.props.setEdgeLabels([
-            ...this.props.edgeLabels,
-            EdgeLabelMode.THROUGHPUT_GROUP,
-            EdgeLabelMode.THROUGHPUT_REQUEST
-          ]);
-        }
-
-        const s = this.getNumericSelector(EdgeAttr.throughput, op, val, expression, isFind);
-        return s ? { target: 'edge', selector: s } : undefined;
-      }
-      default:
-        // special node operand
-        if (field.startsWith('label:')) {
-          return { target: 'node', selector: `[${CytoscapeGraphUtils.toSafeCyFieldName(field)} ${op} "${val}"]` };
-        }
-
-        return this.setError(`Invalid operand [${field}]`, isFind);
-    }
-  };
-
-  private getNumericSelector(
-    field: string,
-    op: string,
-    val: any,
-    _expression: string,
-    isFind: boolean
-  ): string | undefined {
-    switch (op) {
-      case '>':
-      case '<':
-      case '>=':
-      case '<=':
-        if (isNaN(val)) {
-          return this.setError(`Invalid value [${val}]. Expected a numeric value (use '.' for decimals)`, isFind);
-        }
-        return `[${field} ${op} ${val}]`;
-      case '=':
-        if (isNaN(val)) {
-          return `[!${field}]`;
-        }
-        return `[${field} ${op} ${val}]`;
-      case '!=':
-        if (isNaN(val)) {
-          return `[?${field}]`;
-        }
-        return `[${field} ${op} ${val}]`;
-      default:
-        return this.setError(`Invalid operator [${op}] for numeric condition`, isFind);
-    }
-  }
-
-  private parseUnaryFindExpression = (field: string, isNegation): ParsedExpression | undefined => {
-    switch (field.toLowerCase()) {
-      //
-      // nodes...
-      //
-      case 'cb':
-      case 'circuitbreaker':
-        return { target: 'node', selector: isNegation ? `[^${NodeAttr.hasCB}]` : `[?${NodeAttr.hasCB}]` };
-      case 'dead':
-        return { target: 'node', selector: isNegation ? `[^${NodeAttr.isDead}]` : `[?${NodeAttr.isDead}]` };
-      case 'fi':
-      case 'faultinjection':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.hasFaultInjection}]` : `[?${NodeAttr.hasFaultInjection}]`
-        };
-      case 'inaccessible':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.isInaccessible}]` : `[?${NodeAttr.isInaccessible}]`
-        };
-      case 'healthy':
-        return {
-          target: 'node',
-          selector: isNegation
-            ? `[${NodeAttr.healthStatus} != "${HEALTHY.name}"][${NodeAttr.healthStatus} != "${NA.name}"][${NodeAttr.healthStatus} != "${NOT_READY.name}"]`
-            : `[${NodeAttr.healthStatus} = "${HEALTHY.name}"]`
-        };
-      case 'idle':
-        if (!this.props.showIdleNodes) {
-          AlertUtils.addSuccess('Enabling "Idle nodes" display option for graph find/hide expression');
-          this.props.toggleIdleNodes();
-        }
-        return { target: 'node', selector: isNegation ? `[^${NodeAttr.isIdle}]` : `[?${NodeAttr.isIdle}]` };
-      case 'mirroring':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.hasMirroring}]` : `[?${NodeAttr.hasMirroring}]`
-        };
-      case 'outside':
-      case 'outsider':
-        return { target: 'node', selector: isNegation ? `[^${NodeAttr.isOutside}]` : `[?${NodeAttr.isOutside}]` };
-      case 'rr':
-      case 'requestrouting':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.hasRequestRouting}]` : `[?${NodeAttr.hasRequestRouting}]`
-        };
-      case 'rto':
-      case 'requesttimeout':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.hasRequestTimeout}]` : `[?${NodeAttr.hasRequestTimeout}]`
-        };
-      case 'se':
-      case 'serviceentry':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.isServiceEntry}]` : `[?${NodeAttr.isServiceEntry}]`
-        };
-      case 'sc':
-      case 'sidecar':
-        return { target: 'node', selector: isNegation ? `[?${NodeAttr.isOutOfMesh}]` : `[^${NodeAttr.isOutOfMesh}]` };
-      case 'om':
-      case 'outofmesh':
-        return { target: 'node', selector: isNegation ? `[^${NodeAttr.isOutOfMesh}]` : `[?${NodeAttr.isOutOfMesh}]` };
-      case 'tcpts':
-      case 'tcptrafficshifting':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.hasTCPTrafficShifting}]` : `[?${NodeAttr.hasTCPTrafficShifting}]`
-        };
-      case 'ts':
-      case 'trafficshifting':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.hasTrafficShifting}]` : `[?${NodeAttr.hasTrafficShifting}]`
-        };
-      case 'trafficsource':
-      case 'root':
-        return { target: 'node', selector: isNegation ? `[^${NodeAttr.isRoot}]` : `[?${NodeAttr.isRoot}]` };
-      case 'vs':
-      case 'virtualservice':
-        return { target: 'node', selector: isNegation ? `[^${NodeAttr.hasVS}]` : `[?${NodeAttr.hasVS}]` };
-      case 'we':
-      case 'workloadentry':
-        return {
-          target: 'node',
-          selector: isNegation ? `[^${NodeAttr.hasWorkloadEntry}]` : `[?${NodeAttr.hasWorkloadEntry}]`
-        };
-      //
-      // edges...
-      //
-      case 'mtls':
-        if (!this.props.showSecurity) {
-          AlertUtils.addSuccess('Enabling "Security" display option for graph find/hide expression');
-          this.props.toggleGraphSecurity();
-        }
-
-        return { target: 'edge', selector: isNegation ? `[${EdgeAttr.isMTLS} <= 0]` : `[${EdgeAttr.isMTLS} > 0]` };
-      case 'traffic': {
-        return { target: 'edge', selector: isNegation ? `[^${EdgeAttr.hasTraffic}]` : `[?${EdgeAttr.hasTraffic}]` };
-      }
-      default:
-        // special node operand
-        if (field.startsWith('label:')) {
-          const safeFieldName = CytoscapeGraphUtils.toSafeCyFieldName(field);
-          return { target: 'node', selector: isNegation ? `[^${safeFieldName}]` : `[?${safeFieldName}]` };
-        }
-
-        return undefined;
-    }
-  };
-
-  private appendSelector = (
-    selector: string,
-    parsedExpression: ParsedExpression,
-    isFind: boolean
-  ): string | undefined => {
-    if (!selector) {
-      return parsedExpression.target + parsedExpression.selector;
-    }
-
-    if (!selector.startsWith(parsedExpression.target)) {
-      return this.setError('Invalid expression. Can not AND node and edge criteria.', isFind);
-    }
-
-    return selector + parsedExpression.selector;
-  };
-}
-
-const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({
-  edgeLabels: edgeLabelsSelector(state),
-  edgeMode: edgeModeSelector(state),
-  findValue: findValueSelector(state),
-  hideValue: hideValueSelector(state),
-  layout: state.graph.layout,
-  namespaceLayout: state.graph.namespaceLayout,
-  showFindHelp: state.graph.toolbarState.showFindHelp,
-  showIdleNodes: state.graph.toolbarState.showIdleNodes,
-  showRank: state.graph.toolbarState.showRank,
-  showSecurity: state.graph.toolbarState.showSecurity,
-  updateTime: state.graph.updateTime
-});
-
-const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => {
-  return {
-    setEdgeLabels: bindActionCreators(GraphToolbarActions.setEdgeLabels, dispatch),
-    setFindValue: bindActionCreators(GraphToolbarActions.setFindValue, dispatch),
-    setHideValue: bindActionCreators(GraphToolbarActions.setHideValue, dispatch),
-    toggleFindHelp: bindActionCreators(GraphToolbarActions.toggleFindHelp, dispatch),
-    toggleGraphSecurity: bindActionCreators(GraphToolbarActions.toggleGraphSecurity, dispatch),
-    toggleIdleNodes: bindActionCreators(GraphToolbarActions.toggleIdleNodes, dispatch),
-    toggleRank: bindActionCreators(GraphToolbarActions.toggleRank, dispatch)
-  };
-};
-
-export const GraphFind = connect(mapStateToProps, mapDispatchToProps)(GraphFindComponent);
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphFindPF.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphFindPF.tsx
index 44d7e33ba..4f3c4291a 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphFindPF.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphFindPF.tsx
@@ -19,8 +19,7 @@ import { KialiAppState } from '../../../store/Store';
 import { findValueSelector, hideValueSelector, edgeLabelsSelector, edgeModeSelector } from '../../../store/Selectors';
 import { GraphToolbarActions } from '../../../actions/GraphToolbarActions';
 import { GraphHelpFind } from '../../../pages/Graph/GraphHelpFind';
-import * as CytoscapeGraphUtils from '../../../components/CytoscapeGraph/CytoscapeGraphUtils';
-import { EdgeLabelMode, NodeType, Layout, EdgeMode, NodeAttr, EdgeAttr } from '../../../types/Graph';
+import { EdgeLabelMode, NodeType, EdgeMode, NodeAttr, EdgeAttr } from '../../../types/Graph';
 import * as AlertUtils from '../../../utils/AlertUtils';
 import { KialiIcon } from 'config/KialiIcon';
 import { kialiStyle } from 'styles/StyleUtils';
@@ -32,11 +31,11 @@ import { HEALTHY, NA, NOT_READY } from 'types/Health';
 import { GraphFindOptions } from './GraphFindOptions';
 import { location, HistoryManager, URLParam } from '../../../app/History';
 import { isValid } from 'utils/Common';
-import { EdgeData, NodeData } from 'pages/GraphPF/GraphPFElems';
-import { elems, SelectAnd, SelectExp, selectOr, SelectOr, setObserved } from 'helpers/GraphHelpers';
+import { EdgeData, NodeData } from 'pages/Graph/GraphPFElems';
+import { elems, SelectAnd, SelectExp, selectOr, SelectOr, setObserved, toSafeFieldName } from 'helpers/GraphHelpers';
 import { descendents } from 'helpers/GraphHelpers';
 import { isArray } from 'lodash';
-import { graphLayout, LayoutType } from 'pages/GraphPF/GraphPF';
+import { graphLayout, LayoutType } from 'pages/Graph/GraphPF';
 import { infoStyle } from 'styles/IconStyle';
 
 type ReduxStateProps = {
@@ -44,8 +43,6 @@ type ReduxStateProps = {
   edgeMode: EdgeMode;
   findValue: string;
   hideValue: string;
-  layout: Layout;
-  namespaceLayout: Layout;
   showFindHelp: boolean;
   showIdleNodes: boolean;
   showRank: boolean;
@@ -165,7 +162,7 @@ const operands: string[] = [
   'workloadentry'
 ];
 
-class GraphFindPFComponent extends React.Component<GraphFindProps, GraphFindState> {
+export class GraphFindPFComponent extends React.Component<GraphFindProps, GraphFindState> {
   static contextTypes = {
     router: (): null => null
   };
@@ -216,9 +213,9 @@ class GraphFindPFComponent extends React.Component<GraphFindProps, GraphFindStat
     }
   }
 
-  // We only update on a change to the find/hide values, or a graph change.  Although we use other props
-  // in processing (layout, etc), a change to those settings will generate a graph change, so we
-  // wait for the graph change to do the update.
+  // We only update on a change to the find/hide values, or a graph change.  We may use other props
+  // in processing, a change to those settings will generate a graph change, so we wait for the graph
+  // change to force the update.
   shouldComponentUpdate(nextProps: GraphFindProps, nextState: GraphFindState): boolean {
     const controllerChanged = this.props.controller !== nextProps.controller;
     const edgeModeChanged = this.props.edgeMode !== nextProps.edgeMode;
@@ -243,8 +240,8 @@ class GraphFindPFComponent extends React.Component<GraphFindProps, GraphFindStat
   }
 
   // Note that we may have redux hide/find values set at mount-time. But because the toolbar mounts prior to
-  // the graph loading, we can't perform this graph "post-processing" until we have a valid cy graph.  But the
-  // find/hide processing will be initiated externally (CytoscapeGraph:processgraphUpdate) when the graph is ready.
+  // the graph loading, we can't perform this graph "post-processing" until we have a valid graph.  But the
+  // find/hide processing will be initiated externally when the graph is ready.
   componentDidUpdate(prevProps: GraphFindProps): void {
     if (!this.props.controller) {
       this.findElements = undefined;
@@ -1047,7 +1044,7 @@ class GraphFindPFComponent extends React.Component<GraphFindProps, GraphFindStat
         if (field.startsWith('label:')) {
           return {
             target: 'node',
-            selector: { prop: CytoscapeGraphUtils.toSafeCyFieldName(field), op: op, val: val }
+            selector: { prop: toSafeFieldName(field), op: op, val: val }
           };
         }
 
@@ -1190,7 +1187,7 @@ class GraphFindPFComponent extends React.Component<GraphFindProps, GraphFindStat
       default:
         // special node operand
         if (field.startsWith('label:')) {
-          const safeFieldName = CytoscapeGraphUtils.toSafeCyFieldName(field);
+          const safeFieldName = toSafeFieldName(field);
           return { target: 'node', selector: { prop: safeFieldName, op: isNegation ? 'falsy' : 'truthy' } };
         }
 
@@ -1204,8 +1201,6 @@ const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({
   edgeMode: edgeModeSelector(state),
   findValue: findValueSelector(state),
   hideValue: hideValueSelector(state),
-  layout: state.graph.layout,
-  namespaceLayout: state.graph.namespaceLayout,
   showFindHelp: state.graph.toolbarState.showFindHelp,
   showIdleNodes: state.graph.toolbarState.showIdleNodes,
   showRank: state.graph.toolbarState.showRank,
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphShortcuts.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphShortcuts.tsx
deleted file mode 100644
index 9fe5b0db6..000000000
--- a/frontend/src/pages/Graph/GraphToolbar/GraphShortcuts.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import * as React from 'react';
-import { Chip } from '@patternfly/react-core';
-
-interface Shortcut {
-  description: string;
-  shortcut: string;
-}
-
-const shortcuts: Shortcut[] = [
-  { shortcut: 'Mouse wheel', description: 'Zoom' },
-  { shortcut: 'Click + Drag', description: 'Panning' },
-  { shortcut: 'Shift + Drag', description: 'Select zoom area' },
-  { shortcut: 'Right click', description: 'Contextual menu on nodes' },
-  { shortcut: 'Single click', description: 'Details in side panel on nodes and edges' },
-  { shortcut: 'Double click', description: 'Drill into a node details graph' }
-];
-
-const makeShortcut = (shortcut: Shortcut): React.ReactNode => {
-  return (
-    <div style={{ display: 'flex', marginBottom: '10px' }}>
-      <div style={{ flex: '40%' }}>
-        <Chip isReadOnly>{shortcut.shortcut}</Chip>
-      </div>
-
-      <div style={{ flex: '60%' }}>{shortcut.description}</div>
-    </div>
-  );
-};
-
-export const GraphShortcuts = (): React.ReactNode => (
-  <>
-    {shortcuts.map(
-      (s: Shortcut): React.ReactNode => {
-        return makeShortcut(s);
-      }
-    )}
-  </>
-);
diff --git a/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx b/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx
index 551ebb75b..50fbd7a0e 100644
--- a/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/GraphToolbar.tsx
@@ -20,17 +20,8 @@ import {
   trafficRatesSelector
 } from '../../../store/Selectors';
 import { GraphToolbarActions } from '../../../actions/GraphToolbarActions';
-import { GraphFind } from './GraphFind';
 import { GraphSettings } from './GraphSettings';
-import {
-  GraphType,
-  NodeParamsType,
-  EdgeLabelMode,
-  SummaryData,
-  TrafficRate,
-  RankMode,
-  NodeAttr
-} from '../../../types/Graph';
+import { GraphType, NodeParamsType, EdgeLabelMode, SummaryData, TrafficRate, RankMode } from '../../../types/Graph';
 import { router, HistoryManager, URLParam, location } from '../../../app/History';
 import { Namespace, namespacesFromString, namespacesToString } from '../../../types/Namespace';
 import { KialiDispatch } from '../../../types/Redux';
@@ -79,7 +70,6 @@ type GraphToolbarProps = ReduxProps & {
   cy?: any;
   disabled: boolean;
   elementsChanged: boolean;
-  isPF?: boolean;
   onToggleHelp: () => void;
 };
 
@@ -234,19 +224,13 @@ class GraphToolbarComponent extends React.PureComponent<GraphToolbarProps> {
               </TourStop>
             </ToolbarItem>
 
-            {this.props.isPF ? (
-              <ToolbarItem>
-                <GraphFindPF controller={this.props.controller} elementsChanged={this.props.elementsChanged} />
-              </ToolbarItem>
-            ) : (
-              <ToolbarItem>
-                <GraphFind cy={this.props.cy} elementsChanged={this.props.elementsChanged} />
-              </ToolbarItem>
-            )}
+            <ToolbarItem>
+              <GraphFindPF controller={this.props.controller} elementsChanged={this.props.elementsChanged} />
+            </ToolbarItem>
 
             <ToolbarItem style={{ marginLeft: 'auto', alignSelf: 'center' }}>
               <Tooltip key={'graph-tour-help-ot'} position={TooltipPosition.right} content="Shortcuts and tips...">
-                <TourStop info={this.props.isPF ? GraphTourStops.ShortcutsPF : GraphTourStops.Shortcuts}>
+                <TourStop info={GraphTourStops.ShortcutsPF}>
                   <Button
                     id="graph-tour"
                     variant={ButtonVariant.link}
@@ -269,7 +253,7 @@ class GraphToolbarComponent extends React.PureComponent<GraphToolbarProps> {
   }
 
   private handleNamespaceReturn = (): void => {
-    const route = this.props.isPF ? 'graphpf' : 'graph';
+    const route = 'graphpf';
     if (
       !this.props.summaryData ||
       (this.props.summaryData.summaryType !== 'node' && this.props.summaryData.summaryType !== 'box')
@@ -285,9 +269,7 @@ class GraphToolbarComponent extends React.PureComponent<GraphToolbarProps> {
       return;
     }
 
-    const selector = this.props.isPF
-      ? this.props.summaryData!.summaryTarget.getId()
-      : `node[id = "${this.props.summaryData!.summaryTarget.data(NodeAttr.id)}"]`;
+    const selector = this.props.summaryData!.summaryTarget.getId();
 
     this.props.setNode(undefined);
 
diff --git a/frontend/src/pages/Graph/GraphToolbar/__tests__/GraphFind.test.tsx b/frontend/src/pages/Graph/GraphToolbar/__tests__/GraphFind.test.tsx
index f5c4ef610..034d59048 100644
--- a/frontend/src/pages/Graph/GraphToolbar/__tests__/GraphFind.test.tsx
+++ b/frontend/src/pages/Graph/GraphToolbar/__tests__/GraphFind.test.tsx
@@ -1,7 +1,8 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import { GraphFindComponent } from '../GraphFind';
-import { EdgeMode } from 'types/Graph';
+import { GraphFindPFComponent } from '../GraphFindPF';
+import { EdgeLabelMode, EdgeMode } from 'types/Graph';
+import { Controller } from '@patternfly/react-topology';
 
 const testHandler = () => undefined;
 const testSetter = _val => undefined;
@@ -10,9 +11,9 @@ const testSetter = _val => undefined;
 describe('Parse find value test', () => {
   it('should return the correct selector for raw find values', () => {
     const wrapper = shallow(
-      <GraphFindComponent
-        cy={undefined}
-        edgeLabels={[]}
+      <GraphFindPFComponent
+        controller={{} as Controller}
+        edgeLabels={[] as EdgeLabelMode[]}
         edgeMode={EdgeMode.ALL}
         elementsChanged={true}
         findValue="testFind"
@@ -28,268 +29,615 @@ describe('Parse find value test', () => {
         toggleGraphSecurity={testHandler}
         toggleIdleNodes={testHandler}
         toggleRank={testHandler}
-        layout={{ name: '' }}
-        namespaceLayout={{ name: '' }}
-        updateTime={0}
       />
     );
 
-    const instance = wrapper.instance() as GraphFindComponent;
+    const instance = wrapper.instance() as GraphFindPFComponent;
 
     // check coverage of node operands
     // @ts-ignore
-    expect(instance.parseValue('httpin > 5.0')).toEqual('node[httpIn > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('httpout < 5.0')).toEqual('node[httpOut < 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace = foo')).toEqual('node[namespace = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('app = foo')).toEqual('node[app = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns = foo')).toEqual('node[namespace = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = app')).toEqual('node[nodeType = "app"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = operation')).toEqual('node[nodeType = "aggregate"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = op')).toEqual('node[nodeType = "aggregate"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = service')).toEqual('node[nodeType = "service"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = svc')).toEqual('node[nodeType = "service"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = unknown')).toEqual('node[nodeType = "unknown"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = workload')).toEqual('node[nodeType = "workload"]');
-    // @ts-ignore
-    expect(instance.parseValue('node = wl')).toEqual('node[nodeType = "workload"]');
-    // @ts-ignore
-    expect(instance.parseValue('operation = foo')).toEqual('node[aggregateValue = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('op = foo')).toEqual('node[aggregateValue = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('rank = 1')).toEqual('node[rank = 1]');
-    // @ts-ignore
-    expect(instance.parseValue('service = foo')).toEqual('node[service = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('svc = foo')).toEqual('node[service = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('version = foo')).toEqual('node[version = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('tcpin > 5.0')).toEqual('node[tcpIn > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('tcpout < 5.0')).toEqual('node[tcpOut < 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('workload = foo')).toEqual('node[workload = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('wl = foo')).toEqual('node[workload = "foo"]');
+    expect(instance.parseValue('httpin > 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '>', prop: 'httpIn', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('httpout < 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '<', prop: 'httpOut', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('app = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'app', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = app')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'app' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = operation')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'aggregate' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = op')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'aggregate' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = service')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'service' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = svc')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'service' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = unknown')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'unknown' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = workload')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'workload' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('node = wl')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'nodeType', val: 'workload' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('operation = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'aggregateValue', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('op = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'aggregateValue', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('rank = 1')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'rank', val: '1' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('service = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'service', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('svc = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'service', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('version = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'version', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('tcpin > 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '>', prop: 'tcpIn', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('tcpout < 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '<', prop: 'tcpOut', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('workload = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'workload', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('wl = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'workload', val: 'foo' }]]
+    });
 
     // @ts-ignore
-    expect(instance.parseValue('circuitBreaker')).toEqual('node[?hasCB]');
-    // @ts-ignore
-    expect(instance.parseValue('cb')).toEqual('node[?hasCB]');
-    // @ts-ignore
-    expect(instance.parseValue('faultinjection')).toEqual('node[?hasFaultInjection]');
-    // @ts-ignore
-    expect(instance.parseValue('fi')).toEqual('node[?hasFaultInjection]');
-    // @ts-ignore
-    expect(instance.parseValue('mirroring')).toEqual('node[?hasMirroring]');
-    // @ts-ignore
-    expect(instance.parseValue('outside')).toEqual('node[?isOutside]');
-    // @ts-ignore
-    expect(instance.parseValue('outsider')).toEqual('node[?isOutside]');
-    // @ts-ignore
-    expect(instance.parseValue('requestrouting')).toEqual('node[?hasRequestRouting]');
-    // @ts-ignore
-    expect(instance.parseValue('rr')).toEqual('node[?hasRequestRouting]');
-    // @ts-ignore
-    expect(instance.parseValue('requesttimeout')).toEqual('node[?hasRequestTimeout]');
-    // @ts-ignore
-    expect(instance.parseValue('rto')).toEqual('node[?hasRequestTimeout]');
-    // @ts-ignore
-    expect(instance.parseValue('root')).toEqual('node[?isRoot]');
-    // @ts-ignore
-    expect(instance.parseValue('sidecar')).toEqual('node[^isOutOfMesh]');
-    // @ts-ignore
-    expect(instance.parseValue('sc')).toEqual('node[^isOutOfMesh]');
-    // @ts-ignore
-    expect(instance.parseValue('tcptrafficshifting')).toEqual('node[?hasTCPTrafficShifting]');
-    // @ts-ignore
-    expect(instance.parseValue('tcpts')).toEqual('node[?hasTCPTrafficShifting]');
-    // @ts-ignore
-    expect(instance.parseValue('trafficshifting')).toEqual('node[?hasTrafficShifting]');
-    // @ts-ignore
-    expect(instance.parseValue('ts')).toEqual('node[?hasTrafficShifting]');
-    // @ts-ignore
-    expect(instance.parseValue('trafficsource')).toEqual('node[?isRoot]');
-    // @ts-ignore
-    expect(instance.parseValue('virtualService')).toEqual('node[?hasVS]');
-    // @ts-ignore
-    expect(instance.parseValue('vs')).toEqual('node[?hasVS]');
-    // @ts-ignore
-    expect(instance.parseValue('we')).toEqual('node[?hasWorkloadEntry]');
-    // @ts-ignore
-    expect(instance.parseValue('workloadentry')).toEqual('node[?hasWorkloadEntry]');
+    expect(instance.parseValue('circuitBreaker')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasCB' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('cb')).toEqual({ edgeSelector: [], nodeSelector: [[{ op: 'truthy', prop: 'hasCB' }]] });
+    // @ts-ignore
+    expect(instance.parseValue('faultinjection')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasFaultInjection' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('fi')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasFaultInjection' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('mirroring')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasMirroring' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('outside')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'isOutside' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('outsider')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'isOutside' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('requestrouting')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasRequestRouting' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('rr')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasRequestRouting' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('requesttimeout')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasRequestTimeout' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('rto')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasRequestTimeout' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('root')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'isRoot' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('sidecar')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'isOutOfMesh' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('sc')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'isOutOfMesh' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('tcptrafficshifting')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasTCPTrafficShifting' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('tcpts')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasTCPTrafficShifting' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('trafficshifting')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasTrafficShifting' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ts')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasTrafficShifting' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('trafficsource')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'isRoot' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('virtualService')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasVS' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('vs')).toEqual({ edgeSelector: [], nodeSelector: [[{ op: 'truthy', prop: 'hasVS' }]] });
+    // @ts-ignore
+    expect(instance.parseValue('we')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasWorkloadEntry' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('workloadentry')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: 'truthy', prop: 'hasWorkloadEntry' }]]
+    });
 
     // check coverage of edge operands
     // @ts-ignore
-    expect(instance.parseValue('destprincipal contains spiffe')).toEqual('edge[destPrincipal *= "spiffe"]');
-    // @ts-ignore
-    expect(instance.parseValue('grpc > 5.0')).toEqual('edge[grpc > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('%grpcerror > 50')).toEqual('edge[grpcPercentErr > 50]');
-    // @ts-ignore
-    expect(instance.parseValue('%grpcerr > 50')).toEqual('edge[grpcPercentErr > 50]');
-    // @ts-ignore
-    expect(instance.parseValue('%grpctraffic > 50')).toEqual('edge[grpcPercentReq > 50]');
-    // @ts-ignore
-    expect(instance.parseValue('http > 5.0')).toEqual('edge[http > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('%httperror > 50')).toEqual('edge[httpPercentErr > 50]');
-    // @ts-ignore
-    expect(instance.parseValue('%httperr > 50')).toEqual('edge[httpPercentErr > 50]');
-    // @ts-ignore
-    expect(instance.parseValue('%httptraffic > 50')).toEqual('edge[httpPercentReq > 50]');
-    // @ts-ignore
-    expect(instance.parseValue('protocol = http')).toEqual('edge[protocol = "http"]');
-    // @ts-ignore
-    expect(instance.parseValue('responseTime > 5.0')).toEqual('edge[responseTime > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('rt > 5.0')).toEqual('edge[responseTime > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('sourceprincipal contains spiffe')).toEqual('edge[sourcePrincipal *= "spiffe"]');
-    // @ts-ignore
-    expect(instance.parseValue('tcp > 5.0')).toEqual('edge[tcp > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('throughput > 5.0')).toEqual('edge[throughput > 5.0]');
+    expect(instance.parseValue('destprincipal contains spiffe')).toEqual({
+      edgeSelector: [[{ op: '*=', prop: 'destPrincipal', val: 'spiffe' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('grpc > 5.0')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'grpc', val: '5.0' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('%grpcerror > 50')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'grpcPercentErr', val: '50' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('%grpcerr > 50')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'grpcPercentErr', val: '50' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('%grpctraffic > 50')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'grpcPercentReq', val: '50' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('http > 5.0')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'http', val: '5.0' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('%httperror > 50')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'httpPercentErr', val: '50' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('%httperr > 50')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'httpPercentErr', val: '50' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('%httptraffic > 50')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'httpPercentReq', val: '50' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('protocol = http')).toEqual({
+      edgeSelector: [[{ op: '=', prop: 'protocol', val: 'http' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('responseTime > 5.0')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'responseTime', val: '5.0' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('rt > 5.0')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'responseTime', val: '5.0' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('sourceprincipal contains spiffe')).toEqual({
+      edgeSelector: [[{ op: '*=', prop: 'sourcePrincipal', val: 'spiffe' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('tcp > 5.0')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'tcp', val: '5.0' }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('throughput > 5.0')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'throughput', val: '5.0' }]],
+      nodeSelector: []
+    });
 
     // @ts-ignore
-    expect(instance.parseValue('mtls')).toEqual('edge[isMTLS > 0]');
+    expect(instance.parseValue('mtls')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'isMTLS', val: 0 }]],
+      nodeSelector: []
+    });
     // @ts-ignore
-    expect(instance.parseValue('traffic')).toEqual('edge[?hasTraffic]');
+    expect(instance.parseValue('traffic')).toEqual({
+      edgeSelector: [[{ op: 'truthy', prop: 'hasTraffic' }]],
+      nodeSelector: []
+    });
     // @ts-ignore
-    expect(instance.parseValue('!traffic')).toEqual('edge[^hasTraffic]');
+    expect(instance.parseValue('!traffic')).toEqual({
+      edgeSelector: [[{ op: 'falsy', prop: 'hasTraffic' }]],
+      nodeSelector: []
+    });
 
     // check all numeric operators
     // @ts-ignore
-    expect(instance.parseValue('httpin < 5.0')).toEqual('node[httpIn < 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('httpin <= 5.0')).toEqual('node[httpIn <= 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('httpin > 5.0')).toEqual('node[httpIn > 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('httpin >= 5.0')).toEqual('node[httpIn >= 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('httpin = 5.0')).toEqual('node[httpIn = 5.0]');
-    // @ts-ignore
-    expect(instance.parseValue('httpin != 5.0')).toEqual('node[httpIn != 5.0]');
+    expect(instance.parseValue('httpin < 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '<', prop: 'httpIn', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('httpin <= 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '<=', prop: 'httpIn', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('httpin > 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '>', prop: 'httpIn', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('httpin >= 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '>=', prop: 'httpIn', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('httpin = 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'httpIn', val: '5.0' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('httpin != 5.0')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!=', prop: 'httpIn', val: '5.0' }]]
+    });
 
     // check all string operators
     // @ts-ignore
-    expect(instance.parseValue('namespace = foo')).toEqual('node[namespace = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace *= foo')).toEqual('node[namespace *= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace ^= foo')).toEqual('node[namespace ^= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace $= foo')).toEqual('node[namespace $= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace != foo')).toEqual('node[namespace != "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace !*= foo')).toEqual('node[namespace !*= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace !^= foo')).toEqual('node[namespace !^= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace !$= foo')).toEqual('node[namespace !$= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace contains foo')).toEqual('node[namespace *= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace startsWith foo')).toEqual('node[namespace ^= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace endsWith foo')).toEqual('node[namespace $= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace not contains foo')).toEqual('node[namespace !*= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace not startswith foo')).toEqual('node[namespace !^= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('namespace not endswith foo')).toEqual('node[namespace !$= "foo"]');
+    expect(instance.parseValue('namespace = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace *= foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '*=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace ^= foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '^=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace $= foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '$=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace != foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace !*= foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!*=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace !^= foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!^=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace !$= foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!$=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace contains foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '*=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace startsWith foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '^=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace endsWith foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '$=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace not contains foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!*=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace not startswith foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!^=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('namespace not endswith foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!$=', prop: 'namespace', val: 'foo' }]]
+    });
 
     // check unary parsing
     // @ts-ignore
-    expect(instance.parseValue('is mtls')).toEqual('edge[isMTLS > 0]');
-    // @ts-ignore
-    expect(instance.parseValue('has mtls')).toEqual('edge[isMTLS > 0]');
-    // @ts-ignore
-    expect(instance.parseValue('! mtls')).toEqual('edge[isMTLS <= 0]');
-    // @ts-ignore
-    expect(instance.parseValue('!has mtls')).toEqual('edge[isMTLS <= 0]');
-    // @ts-ignore
-    expect(instance.parseValue('!mtls')).toEqual('edge[isMTLS <= 0]');
-    // @ts-ignore
-    expect(instance.parseValue('not has mtls')).toEqual('edge[isMTLS <= 0]');
+    expect(instance.parseValue('is mtls')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'isMTLS', val: 0 }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('has mtls')).toEqual({
+      edgeSelector: [[{ op: '>', prop: 'isMTLS', val: 0 }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('! mtls')).toEqual({
+      edgeSelector: [[{ op: '<=', prop: 'isMTLS', val: 0 }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('!has mtls')).toEqual({
+      edgeSelector: [[{ op: '<=', prop: 'isMTLS', val: 0 }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('!mtls')).toEqual({
+      edgeSelector: [[{ op: '<=', prop: 'isMTLS', val: 0 }]],
+      nodeSelector: []
+    });
+    // @ts-ignore
+    expect(instance.parseValue('not has mtls')).toEqual({
+      edgeSelector: [[{ op: '<=', prop: 'isMTLS', val: 0 }]],
+      nodeSelector: []
+    });
 
     // check binary parsing
     // @ts-ignore
-    expect(instance.parseValue('ns =foo')).toEqual('node[namespace = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns= foo')).toEqual('node[namespace = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns  =  foo')).toEqual('node[namespace = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns=foo')).toEqual('node[namespace = "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns not =foo')).toEqual('node[namespace != "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns!=foo')).toEqual('node[namespace != "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns not contains foo')).toEqual('node[namespace !*= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns !contains foo')).toEqual('node[namespace !*= "foo"]');
-    // @ts-ignore
-    expect(instance.parseValue('ns ! contains foo')).toEqual('node[namespace !*= "foo"]');
+    expect(instance.parseValue('ns =foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns= foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns  =  foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns=foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns not =foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns!=foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns not contains foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!*=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns !contains foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!*=', prop: 'namespace', val: 'foo' }]]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('ns ! contains foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '!*=', prop: 'namespace', val: 'foo' }]]
+    });
 
     // check composites
+
+    // OR same target
     // @ts-ignore
-    expect(instance.parseValue('ns=foo OR ns=bar')).toEqual('node[namespace = "foo"],node[namespace = "bar"]'); // OR same target
-    // @ts-ignore
-    expect(instance.parseValue('ns=foo AND ns=bar')).toEqual('node[namespace = "foo"][namespace = "bar"]'); // AND same target
-    // @ts-ignore
-    expect(instance.parseValue('ns=foo OR ns=bar AND app=foo')).toEqual(
-      'node[namespace = "foo"],node[namespace = "bar"][app = "foo"]'
-    ); // AND and OR same target
-    // @ts-ignore
-    expect(instance.parseValue('ns=foo OR protocol=http')).toEqual('node[namespace = "foo"],edge[protocol = "http"]'); // OR different targets
+    expect(instance.parseValue('ns=foo OR ns=bar')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }], [{ op: '=', prop: 'namespace', val: 'bar' }]]
+    });
+
+    // AND same target
+    // @ts-ignore
+    expect(instance.parseValue('ns=foo AND ns=bar')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [
+        [
+          { op: '=', prop: 'namespace', val: 'foo' },
+          { op: '=', prop: 'namespace', val: 'bar' }
+        ]
+      ]
+    });
+
+    // AND and OR same target
+    // @ts-ignore
+    expect(instance.parseValue('ns=foo OR ns=bar AND app=foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [
+        [{ op: '=', prop: 'namespace', val: 'foo' }],
+        [
+          { op: '=', prop: 'namespace', val: 'bar' },
+          { op: '=', prop: 'app', val: 'foo' }
+        ]
+      ]
+    });
+
+    // OR different targets
     // @ts-ignore
-    expect(instance.parseValue('ns=foo OR protocol=http OR !traffic')).toEqual(
-      'node[namespace = "foo"],edge[protocol = "http"],edge[^hasTraffic]'
-    ); // OR different targets
+    expect(instance.parseValue('ns=foo OR protocol=http')).toEqual({
+      edgeSelector: [[{ op: '=', prop: 'protocol', val: 'http' }]],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+
+    // OR different targets
     // @ts-ignore
-    expect(instance.parseValue('ns=foo AND ns=bar OR protocol=http AND !traffic')).toEqual(
-      'node[namespace = "foo"][namespace = "bar"],edge[protocol = "http"][^hasTraffic]'
-    ); // OR different targets, each with AND
+    expect(instance.parseValue('ns=foo OR protocol=http OR !traffic')).toEqual({
+      edgeSelector: [[{ op: '=', prop: 'protocol', val: 'http' }], [{ op: 'falsy', prop: 'hasTraffic' }]],
+      nodeSelector: [[{ op: '=', prop: 'namespace', val: 'foo' }]]
+    });
+
+    // OR different targets, each with AND
+    // @ts-ignore
+    expect(instance.parseValue('ns=foo AND ns=bar OR protocol=http AND !traffic')).toEqual({
+      edgeSelector: [
+        [
+          { op: '=', prop: 'protocol', val: 'http' },
+          { op: 'falsy', prop: 'hasTraffic' }
+        ]
+      ],
+      nodeSelector: [
+        [
+          { op: '=', prop: 'namespace', val: 'foo' },
+          { op: '=', prop: 'namespace', val: 'bar' }
+        ]
+      ]
+    });
 
     // check find by name
     // @ts-ignore
-    expect(instance.parseValue('name = foo')).toEqual(
-      'node[aggregateValue = "foo"],[app = "foo"],[service = "foo"],[workload = "foo"]'
-    );
-    // @ts-ignore
-    expect(instance.parseValue('name != foo')).toEqual(
-      'node[aggregateValue != "foo"][app != "foo"][service != "foo"][workload != "foo"]'
-    );
+    expect(instance.parseValue('name = foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [
+        [{ op: '=', prop: 'aggregateValue', val: 'foo' }],
+        [{ op: '=', prop: 'app', val: 'foo' }],
+        [{ op: '=', prop: 'service', val: 'foo' }],
+        [{ op: '=', prop: 'workload', val: 'foo' }]
+      ]
+    });
+    // @ts-ignore
+    expect(instance.parseValue('name != foo')).toEqual({
+      edgeSelector: [],
+      nodeSelector: [
+        [
+          { op: '!=', prop: 'aggregateValue', val: 'foo' },
+          { op: '!=', prop: 'app', val: 'foo' },
+          { op: '!=', prop: 'service', val: 'foo' },
+          { op: '!=', prop: 'workload', val: 'foo' }
+        ]
+      ]
+    });
 
     // check violations
     // @ts-ignore
-    expect(instance.parseValue('foo')).toEqual(undefined); // invalid unary
+    expect(instance.parseValue('foo')).toEqual({ edgeSelector: undefined, nodeSelector: undefined }); // invalid unary
     // @ts-ignore
-    expect(instance.parseValue('!foo')).toEqual(undefined); // invalid negated unary
+    expect(instance.parseValue('!foo')).toEqual({ edgeSelector: undefined, nodeSelector: undefined }); // invalid negated unary
     // @ts-ignore
-    expect(instance.parseValue('node = appp')).toEqual(undefined); // invalid node type
+    expect(instance.parseValue('node = appp')).toEqual({ edgeSelector: undefined, nodeSelector: undefined }); // invalid node type
     // @ts-ignore
-    expect(instance.parseValue('ns=foo AND http > 5.0')).toEqual(undefined); // Node and Edge
+    expect(instance.parseValue('ns=foo AND http > 5.0')).toEqual({ edgeSelector: undefined, nodeSelector: undefined }); // Node and Edge
     // @ts-ignore
-    expect(instance.parseValue('rank = a')).toEqual(undefined); // not a number
+    expect(instance.parseValue('rank = a')).toEqual({ edgeSelector: undefined, nodeSelector: undefined }); // not a number
     // @ts-ignore
-    expect(instance.parseValue('rank = 101')).toEqual(undefined); // outside acceptable range
+    expect(instance.parseValue('rank = 101')).toEqual({ edgeSelector: undefined, nodeSelector: undefined }); // outside acceptable range
   });
 });
diff --git a/frontend/src/pages/GraphPF/MiniGraphCardPF.tsx b/frontend/src/pages/Graph/MiniGraphCardPF.tsx
similarity index 97%
rename from frontend/src/pages/GraphPF/MiniGraphCardPF.tsx
rename to frontend/src/pages/Graph/MiniGraphCardPF.tsx
index 9bf9a32a1..334fe0d7f 100644
--- a/frontend/src/pages/GraphPF/MiniGraphCardPF.tsx
+++ b/frontend/src/pages/Graph/MiniGraphCardPF.tsx
@@ -15,13 +15,13 @@ import {
 import { Edge, EdgeModel, Node, NodeModel } from '@patternfly/react-topology';
 import { URLParam, location, router } from '../../app/History';
 import { GraphDataSource } from '../../services/GraphDataSource';
-import { DecoratedGraphElements, EdgeMode, GraphEvent, GraphType, Layout, NodeType } from '../../types/Graph';
+import { DecoratedGraphElements, EdgeMode, GraphEvent, GraphType, NodeType } from '../../types/Graph';
 import { GraphUrlParams, makeNodeGraphUrlFromParams } from 'components/Nav/NavUtils';
 import { store } from 'store/ConfigStore';
 import { TimeInMilliseconds } from '../../types/Common';
 import { ServiceDetailsInfo } from '../../types/ServiceInfo';
 import { KialiAppState } from '../../store/Store';
-import { GraphPF } from './GraphPF';
+import { GraphLayout, GraphPF } from './GraphPF';
 import { WizardAction, WizardMode } from 'components/IstioWizards/WizardActions';
 import { isKiosk, isParentKiosk, kioskContextMenuAction } from 'components/Kiosk/KioskActions';
 import { ServiceWizardActionsDropdownGroup } from 'components/IstioWizards/ServiceWizardActionsDropdownGroup';
@@ -29,7 +29,6 @@ import { toRangeString } from 'components/Time/Utils';
 import { KioskElement } from 'components/Kiosk/KioskElement';
 import { TimeDurationIndicator } from 'components/Time/TimeDurationIndicator';
 import { TimeDurationModal } from 'components/Time/TimeDurationModal';
-import { KialiDagreGraph } from 'components/CytoscapeGraph/graphs/KialiDagreGraph';
 import { KialiDispatch } from 'types/Redux';
 import { bindActionCreators } from 'redux';
 import { GraphActions } from 'actions/GraphActions';
@@ -41,11 +40,11 @@ import { kebabToggleStyle } from 'styles/DropdownStyles';
 import { WorkloadWizardActionsDropdownGroup } from 'components/IstioWizards/WorkloadWizardActionsDropdownGroup';
 import { Workload } from 'types/Workload';
 import { GraphRefs } from './GraphPagePF';
-import { EmptyGraphLayout } from 'components/CytoscapeGraph/EmptyGraphLayout';
+import { EmptyGraphLayout } from 'pages/Graph/EmptyGraphLayout';
 
 type ReduxDispatchProps = {
   setEdgeMode: (edgeMode: EdgeMode) => void;
-  setLayout: (layout: Layout) => void;
+  setLayout: (layout: GraphLayout) => void;
   setUpdateTime: (val: TimeInMilliseconds) => void;
   updateSummary: (event: GraphEvent) => void;
 };
@@ -209,7 +208,7 @@ class MiniGraphCardPFComponent extends React.Component<MiniGraphCardPropsPF, Min
                     timestamp: this.props.dataSource.graphTimestamp
                   }}
                   isMiniGraph={true}
-                  layout={KialiDagreGraph.getLayout()}
+                  layout={GraphLayout.Dagre}
                   onDeleteTrafficRouting={this.handleDeleteTrafficRouting}
                   onEdgeTap={this.handleEdgeTap}
                   onLaunchWizard={this.handleLaunchWizard}
@@ -440,9 +439,9 @@ class MiniGraphCardPFComponent extends React.Component<MiniGraphCardPropsPF, Min
 
     // To ensure updated components get the updated URL, update the URL first and then the state
     if (isParentKiosk(this.props.kiosk)) {
-      kioskContextMenuAction(makeNodeGraphUrlFromParams(urlParams, true));
+      kioskContextMenuAction(makeNodeGraphUrlFromParams(urlParams));
     } else {
-      router.navigate(makeNodeGraphUrlFromParams(urlParams, true));
+      router.navigate(makeNodeGraphUrlFromParams(urlParams));
     }
   };
 
diff --git a/frontend/src/pages/GraphPF/NodeDecorator.tsx b/frontend/src/pages/Graph/NodeDecorator.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/NodeDecorator.tsx
rename to frontend/src/pages/Graph/NodeDecorator.tsx
diff --git a/frontend/src/pages/Graph/SummaryPanel.tsx b/frontend/src/pages/Graph/SummaryPanel.tsx
index 54b63bf1a..07abf1e5a 100644
--- a/frontend/src/pages/Graph/SummaryPanel.tsx
+++ b/frontend/src/pages/Graph/SummaryPanel.tsx
@@ -18,7 +18,7 @@ import { summaryPanelWidth } from './SummaryPanelCommon';
 import { WizardAction, WizardMode } from 'components/IstioWizards/WizardActions';
 import { ServiceDetailsInfo } from '../../types/ServiceInfo';
 import { PeerAuthentication } from '../../types/IstioObjects';
-import { FocusNode } from 'pages/GraphPF/GraphPF';
+import { FocusNode } from 'pages/Graph/GraphPF';
 import { classes } from 'typestyle';
 import { panelBodyStyle, panelStyle } from './SummaryPanelStyle';
 import { PFColors } from 'components/Pf/PfColors';
@@ -113,15 +113,9 @@ class SummaryPanelComponent extends React.Component<MainSummaryPanelPropType, Su
       : collapsedStyle;
 
     let tourStops = [GraphTourStops.SidePanel];
-    const summary: SummaryData = this.props.data;
 
-    if (summary.isPF) {
-      tourStops.unshift(GraphTourStops.GraphPF);
-      tourStops.unshift(GraphTourStops.ContextualMenuPF);
-    } else {
-      tourStops.unshift(GraphTourStops.Graph);
-      tourStops.unshift(GraphTourStops.ContextualMenu);
-    }
+    tourStops.unshift(GraphTourStops.GraphPF);
+    tourStops.unshift(GraphTourStops.ContextualMenuPF);
 
     return (
       <TourStop info={tourStops}>
@@ -161,14 +155,11 @@ class SummaryPanelComponent extends React.Component<MainSummaryPanelPropType, Su
   }
 
   private getSummaryPanel = (summary: SummaryData): React.ReactFragment => {
-    const isPF = !!summary.isPF;
     const summaryType = summary.summaryType as string;
 
     switch (summaryType) {
       case 'box': {
-        const boxType: BoxByType = isPF
-          ? summary.summaryTarget.getData()[NodeAttr.isBox]
-          : summary.summaryTarget.data(NodeAttr.isBox);
+        const boxType: BoxByType = summary.summaryTarget.getData()[NodeAttr.isBox];
         switch (boxType) {
           case 'app':
             return (
diff --git a/frontend/src/pages/Graph/SummaryPanelAppBox.tsx b/frontend/src/pages/Graph/SummaryPanelAppBox.tsx
index 335b442f7..cf33c8304 100644
--- a/frontend/src/pages/Graph/SummaryPanelAppBox.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelAppBox.tsx
@@ -28,8 +28,7 @@ import { IstioMetricsMap, Datapoint, Labels } from '../../types/Metrics';
 import { Reporter } from '../../types/MetricsOptions';
 import { CancelablePromise, makeCancelablePromise } from '../../utils/CancelablePromises';
 import { KialiIcon } from 'config/KialiIcon';
-import { decoratedNodeData } from 'components/CytoscapeGraph/CytoscapeGraphUtils';
-import { getOptions, clickHandler } from 'components/CytoscapeGraph/ContextMenu/NodeContextMenu';
+import { getOptions, clickHandler } from 'pages/Graph/ContextMenu/NodeContextMenu';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { edgesIn, edgesOut, select, selectAnd, selectOr } from 'helpers/GraphHelpers';
 import { classes } from 'typestyle';
@@ -150,22 +149,21 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
   }
 
   render(): React.ReactNode {
-    const isPF = !!this.props.data.isPF;
     const appBox = this.props.data.summaryTarget;
-    const nodeData = isPF ? appBox.getData() : decoratedNodeData(appBox);
+    const nodeData = appBox.getData();
 
-    const serviceList = this.renderServiceList(appBox, isPF);
-    const workloadList = this.renderWorkloadList(appBox, isPF);
-    const hasGrpc = this.hasGrpcTraffic(appBox, isPF);
+    const serviceList = this.renderServiceList(appBox);
+    const workloadList = this.renderWorkloadList(appBox);
+    const hasGrpc = this.hasGrpcTraffic(appBox);
     const isGrpcRequests = hasGrpc && this.isGrpcRequests();
-    const hasGrpcIn = hasGrpc && this.hasGrpcIn(appBox, isPF);
-    const hasGrpcOut = hasGrpc && this.hasGrpcOut(appBox, isPF);
-    const hasHttp = this.hasHttpTraffic(appBox, isPF);
-    const hasHttpIn = hasHttp && this.hasHttpIn(appBox, isPF);
-    const hasHttpOut = hasHttp && this.hasHttpOut(appBox, isPF);
-    const hasTcp = this.hasTcpTraffic(appBox, isPF);
-    const hasTcpIn = hasTcp && this.hasTcpIn(appBox, isPF);
-    const hasTcpOut = hasTcp && this.hasTcpOut(appBox, isPF);
+    const hasGrpcIn = hasGrpc && this.hasGrpcIn(appBox);
+    const hasGrpcOut = hasGrpc && this.hasGrpcOut(appBox);
+    const hasHttp = this.hasHttpTraffic(appBox);
+    const hasHttpIn = hasHttp && this.hasHttpIn(appBox);
+    const hasHttpOut = hasHttp && this.hasHttpOut(appBox);
+    const hasTcp = this.hasTcpTraffic(appBox);
+    const hasTcpIn = hasTcp && this.hasTcpIn(appBox);
+    const hasTcpOut = hasTcp && this.hasTcpOut(appBox);
 
     const options = getOptions(nodeData);
     const items = [
@@ -243,7 +241,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
           </span>
 
           <div>
-            {this.renderBadgeSummary(appBox, isPF)}
+            {this.renderBadgeSummary(appBox)}
             {serviceList.length > 0 && <div>{serviceList}</div>}
             {workloadList.length > 0 && <div> {workloadList}</div>}
           </div>
@@ -252,20 +250,20 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
         <div className={panelBodyStyle}>
           {hasGrpc && isGrpcRequests && (
             <>
-              {this.renderGrpcRequests(appBox, isPF)}
+              {this.renderGrpcRequests(appBox)}
               {hr()}
             </>
           )}
 
           {hasHttp && (
             <>
-              {this.renderHttpRequests(appBox, isPF)}
+              {this.renderHttpRequests(appBox)}
               {hr()}
             </>
           )}
 
           <div>
-            {this.renderSparklines(appBox, isPF)}
+            {this.renderSparklines(appBox)}
             {hr()}
           </div>
 
@@ -288,9 +286,8 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
   };
 
   private updateCharts = (props: SummaryPanelPropType): void => {
-    const isPF = !!this.props.data.isPF;
     const appBox = props.data.summaryTarget;
-    const nodeData = isPF ? (appBox.getData() as DecoratedGraphNodeData) : decoratedNodeData(appBox);
+    const nodeData = appBox.getData() as DecoratedGraphNodeData;
     const nodeMetricType = getNodeMetricType(nodeData);
     const isGrpcRequests = this.isGrpcRequests();
 
@@ -299,7 +296,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
       this.metricsPromise = undefined;
     }
 
-    if (!this.hasGrpcTraffic(appBox, isPF) && !this.hasHttpTraffic(appBox, isPF) && !this.hasTcpTraffic(appBox, isPF)) {
+    if (!this.hasGrpcTraffic(appBox) && !this.hasHttpTraffic(appBox) && !this.hasTcpTraffic(appBox)) {
       this.setState({ loading: false });
       return;
     }
@@ -308,7 +305,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     let promiseInRps: Promise<ApiResponse<IstioMetricsMap>> = Promise.resolve({ data: {} });
     let promiseInStream: Promise<ApiResponse<IstioMetricsMap>> = Promise.resolve({ data: {} });
 
-    if (this.hasHttpIn(appBox, isPF) || (this.hasGrpcIn(appBox, isPF) && isGrpcRequests, isPF)) {
+    if (this.hasHttpIn(appBox) || (this.hasGrpcIn(appBox) && isGrpcRequests)) {
       const filtersRps = ['request_count', 'request_error_count'];
 
       promiseInRps = getNodeMetrics(
@@ -325,14 +322,14 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
       );
     }
 
-    if (this.hasTcpIn(appBox, isPF) || (this.hasGrpcIn(appBox, isPF) && !isGrpcRequests)) {
+    if (this.hasTcpIn(appBox) || (this.hasGrpcIn(appBox) && !isGrpcRequests)) {
       const filtersStream = [] as string[];
 
-      if (this.hasGrpcIn(appBox, isPF) && !isGrpcRequests) {
+      if (this.hasGrpcIn(appBox) && !isGrpcRequests) {
         filtersStream.push('grpc_sent', 'grpc_received');
       }
 
-      if (this.hasTcpIn(appBox, isPF)) {
+      if (this.hasTcpIn(appBox)) {
         filtersStream.push('tcp_sent', 'tcp_received');
       }
 
@@ -360,15 +357,15 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     // Ignore outbound traffic if it is a non-root (appbox is always non-root) outsider (because they have no outbound edges)
     if (!nodeData.isOutside) {
       const filters = [] as string[];
-      if (this.hasHttpOut(appBox, isPF) || (this.hasGrpcOut(appBox, isPF) && isGrpcRequests)) {
+      if (this.hasHttpOut(appBox) || (this.hasGrpcOut(appBox) && isGrpcRequests)) {
         filters.push('request_count', 'request_error_count');
       }
 
-      if (this.hasGrpcOut(appBox, isPF) && !isGrpcRequests) {
+      if (this.hasGrpcOut(appBox) && !isGrpcRequests) {
         filters.push('grpc_sent', 'grpc_received');
       }
 
-      if (this.hasTcpOut(appBox, isPF)) {
+      if (this.hasTcpOut(appBox)) {
         filters.push('tcp_sent', 'tcp_received');
       }
 
@@ -479,42 +476,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     return false;
   };
 
-  private renderBadgeSummary = (appBox, isPF: boolean): React.ReactNode => {
-    if (isPF) {
-      return this.renderBadgeSummaryPF(appBox);
-    }
-
-    let hasCB: boolean = appBox.data(NodeAttr.hasCB) === true;
-    let hasVS: boolean = appBox.data(NodeAttr.hasVS) === true;
-
-    appBox
-      .children(`node[${NodeAttr.hasCB}],[${NodeAttr.hasVS}]`)
-      .nodes()
-      .forEach((n: any) => {
-        hasCB = hasCB || n.data(NodeAttr.hasCB);
-        hasVS = hasVS || n.data(NodeAttr.hasVS);
-      });
-
-    return (
-      <div style={{ marginTop: '0.5rem', marginBottom: '0.5rem' }}>
-        {hasCB && (
-          <div>
-            <KialiIcon.CircuitBreaker />
-            <span style={{ paddingLeft: '0.25rem' }}>Has Circuit Breaker</span>
-          </div>
-        )}
-
-        {hasVS && (
-          <div>
-            <KialiIcon.VirtualService />
-            <span style={{ paddingLeft: '0.25rem' }}>Has Virtual Service</span>
-          </div>
-        )}
-      </div>
-    );
-  };
-
-  private renderBadgeSummaryPF = (appBox: Node): React.ReactNode => {
+  private renderBadgeSummary = (appBox): React.ReactNode => {
     const appBoxData = appBox.getData();
 
     let hasCB: boolean = appBoxData[NodeAttr.hasCB] === true;
@@ -549,33 +511,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     );
   };
 
-  private renderGrpcRequests = (appBox, isPF: boolean): React.ReactNode => {
-    if (isPF) {
-      return this.renderGrpcRequestsPF(appBox);
-    }
-
-    // only consider the physical children to avoid inflated rates
-    const validChildren = appBox.children(
-      `node[nodeType != "${NodeType.SERVICE}"][nodeType != "${NodeType.AGGREGATE}"]`
-    );
-
-    const inbound = getAccumulatedTrafficRateGrpc(validChildren.incomers('edge'));
-    const outbound = getAccumulatedTrafficRateGrpc(validChildren.edgesTo('*'));
-
-    return (
-      <InOutRateTableGrpc
-        title="GRPC Traffic (requests per second):"
-        inRate={inbound.rate}
-        inRateGrpcErr={inbound.rateGrpcErr}
-        inRateNR={inbound.rateNoResponse}
-        outRate={outbound.rate}
-        outRateGrpcErr={outbound.rateGrpcErr}
-        outRateNR={outbound.rateNoResponse}
-      />
-    );
-  };
-
-  private renderGrpcRequestsPF = (appBox: Node): React.ReactNode => {
+  private renderGrpcRequests = (appBox): React.ReactNode => {
     // only consider the physical children to avoid inflated rates
     const appBoxChildren = appBox.getAllNodeChildren();
 
@@ -584,8 +520,8 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
       { prop: NodeAttr.nodeType, op: '!=', val: NodeType.AGGREGATE }
     ]);
 
-    const inbound = getAccumulatedTrafficRateGrpc(edgesIn(validChildren as Node[]), true);
-    const outbound = getAccumulatedTrafficRateGrpc(edgesOut(validChildren as Node[]), true);
+    const inbound = getAccumulatedTrafficRateGrpc(edgesIn(validChildren as Node[]));
+    const outbound = getAccumulatedTrafficRateGrpc(edgesOut(validChildren as Node[]));
 
     return (
       <InOutRateTableGrpc
@@ -600,37 +536,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     );
   };
 
-  private renderHttpRequests = (appBox, isPF: boolean): React.ReactNode => {
-    if (isPF) {
-      return this.renderHttpRequestsPF(appBox);
-    }
-
-    // only consider the physical children to avoid inflated rates
-    const validChildren = appBox.children(
-      `node[nodeType != "${NodeType.SERVICE}"][nodeType != "${NodeType.AGGREGATE}"]`
-    );
-
-    const inbound = getAccumulatedTrafficRateHttp(validChildren.incomers('edge'));
-    const outbound = getAccumulatedTrafficRateHttp(validChildren.edgesTo('*'));
-
-    return (
-      <InOutRateTableHttp
-        title="HTTP (requests per second):"
-        inRate={inbound.rate}
-        inRate3xx={inbound.rate3xx}
-        inRate4xx={inbound.rate4xx}
-        inRate5xx={inbound.rate5xx}
-        inRateNR={inbound.rateNoResponse}
-        outRate={outbound.rate}
-        outRate3xx={outbound.rate3xx}
-        outRate4xx={outbound.rate4xx}
-        outRate5xx={outbound.rate5xx}
-        outRateNR={outbound.rateNoResponse}
-      />
-    );
-  };
-
-  private renderHttpRequestsPF = (appBox: Node): React.ReactNode => {
+  private renderHttpRequests = (appBox): React.ReactNode => {
     // only consider the physical children to avoid inflated rates
     const appBoxChildren = appBox.getAllNodeChildren();
 
@@ -639,8 +545,8 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
       { prop: NodeAttr.nodeType, op: '!=', val: NodeType.AGGREGATE }
     ]);
 
-    const inbound = getAccumulatedTrafficRateHttp(edgesIn(validChildren as Node[]), true);
-    const outbound = getAccumulatedTrafficRateHttp(edgesOut(validChildren as Node[]), true);
+    const inbound = getAccumulatedTrafficRateHttp(edgesIn(validChildren as Node[]));
+    const outbound = getAccumulatedTrafficRateHttp(edgesOut(validChildren as Node[]));
 
     return (
       <InOutRateTableHttp
@@ -659,7 +565,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     );
   };
 
-  private renderSparklines = (appBox, isPF: boolean): React.ReactNode => {
+  private renderSparklines = (appBox): React.ReactNode => {
     if (this.state.loading) {
       return <strong>Loading charts...</strong>;
     } else if (this.state.metricsLoadError) {
@@ -671,16 +577,16 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
       );
     }
 
-    const hasGrpc = this.hasGrpcTraffic(appBox, isPF);
+    const hasGrpc = this.hasGrpcTraffic(appBox);
     const isGrpcRequests = hasGrpc && this.isGrpcRequests();
-    const hasGrpcIn = hasGrpc && this.hasGrpcIn(appBox, isPF);
-    const hasGrpcOut = hasGrpc && this.hasGrpcOut(appBox, isPF);
-    const hasHttp = this.hasHttpTraffic(appBox, isPF);
-    const hasHttpIn = hasHttp && this.hasHttpIn(appBox, isPF);
-    const hasHttpOut = hasHttp && this.hasHttpOut(appBox, isPF);
-    const hasTcp = this.hasTcpTraffic(appBox, isPF);
-    const hasTcpIn = hasTcp && this.hasTcpIn(appBox, isPF);
-    const hasTcpOut = hasTcp && this.hasTcpOut(appBox, isPF);
+    const hasGrpcIn = hasGrpc && this.hasGrpcIn(appBox);
+    const hasGrpcOut = hasGrpc && this.hasGrpcOut(appBox);
+    const hasHttp = this.hasHttpTraffic(appBox);
+    const hasHttpIn = hasHttp && this.hasHttpIn(appBox);
+    const hasHttpOut = hasHttp && this.hasHttpOut(appBox);
+    const hasTcp = this.hasTcpTraffic(appBox);
+    const hasTcpIn = hasTcp && this.hasTcpIn(appBox);
+    const hasTcpOut = hasTcp && this.hasTcpOut(appBox);
 
     let grpcCharts: React.ReactNode, httpCharts: React.ReactNode, tcpCharts: React.ReactNode;
 
@@ -787,38 +693,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     );
   };
 
-  private renderServiceList = (appBox, isPF: boolean): React.ReactNode[] => {
-    if (isPF) {
-      return this.renderServiceListPF(appBox);
-    }
-
-    // likely 0 or 1 but support N in case of unanticipated labeling
-    const serviceList: React.ReactNode[] = [];
-
-    appBox.children(`node[nodeType = "${NodeType.SERVICE}"]`).forEach((serviceNode, i) => {
-      const serviceNodeData = isPF ? serviceNode.getData() : decoratedNodeData(serviceNode);
-      serviceList.push(renderBadgedLink(serviceNodeData, NodeType.SERVICE));
-
-      const aggregates = appBox.children(
-        `node[nodeType = "${NodeType.AGGREGATE}"][service = "${serviceNodeData.service}"]`
-      );
-
-      if (!!aggregates && aggregates.length > 0) {
-        const aggregateList: React.ReactNode[] = [];
-
-        aggregates.forEach((aggregateNode: any) => {
-          const aggregateNodeData = isPF ? aggregateNode.getData() : decoratedNodeData(aggregateNode);
-          aggregateList.push(renderBadgedLink(aggregateNodeData, NodeType.AGGREGATE));
-        });
-
-        serviceList.push(<div key={`service-${i}`}>{aggregateList}</div>);
-      }
-    });
-
-    return serviceList;
-  };
-
-  private renderServiceListPF = (appBox: Node): React.ReactNode[] => {
+  private renderServiceList = (appBox): React.ReactNode[] => {
     // likely 0 or 1 but support N in case of unanticipated labeling
     const serviceList: React.ReactNode[] = [];
 
@@ -848,22 +723,7 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     return serviceList;
   };
 
-  private renderWorkloadList = (appBox, isPF: boolean): React.ReactNode[] => {
-    if (isPF) {
-      return this.renderWorkloadListPF(appBox);
-    }
-
-    const workloadList: React.ReactNode[] = [];
-
-    appBox.children('node[workload]').forEach(node => {
-      const nodeData = isPF ? node.getData() : decoratedNodeData(node);
-      workloadList.push(renderBadgedLink(nodeData, NodeType.WORKLOAD));
-    });
-
-    return workloadList;
-  };
-
-  private renderWorkloadListPF = (appBox: Node): React.ReactNode[] => {
+  private renderWorkloadList = (appBox): React.ReactNode[] => {
     const workloadList: React.ReactNode[] = [];
 
     const appBoxChildren = appBox.getAllNodeChildren();
@@ -880,141 +740,103 @@ export class SummaryPanelAppBox extends React.Component<SummaryPanelPropType, Su
     return this.props.trafficRates.includes(TrafficRate.GRPC_REQUEST);
   };
 
-  private hasGrpcTraffic = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
-      const notServices = select(appBoxChildren, { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE });
-
-      return (
-        selectOr(notServices, [
-          [{ prop: NodeAttr.grpcIn, op: '>', val: 0 }],
-          [{ prop: NodeAttr.grpcOut, op: '>', val: 0 }]
-        ]).length > 0
-      );
-    }
+  private hasGrpcTraffic = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
+    const notServices = select(appBoxChildren, { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE });
 
-    return appBox.children().filter('[nodeType != "service"]').filter('[grpcIn > 0],[grpcOut > 0]').size() > 0;
+    return (
+      selectOr(notServices, [
+        [{ prop: NodeAttr.grpcIn, op: '>', val: 0 }],
+        [{ prop: NodeAttr.grpcOut, op: '>', val: 0 }]
+      ]).length > 0
+    );
   };
 
-  private hasGrpcIn = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
+  private hasGrpcIn = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
 
-      return (
-        selectAnd(appBoxChildren, [
-          { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
-          { prop: NodeAttr.grpcIn, op: '>', val: 0 }
-        ]).length > 0
-      );
-    }
-
-    return appBox.children().filter('[nodeType != "service"]').filter('[grpcIn > 0]').size() > 0;
+    return (
+      selectAnd(appBoxChildren, [
+        { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
+        { prop: NodeAttr.grpcIn, op: '>', val: 0 }
+      ]).length > 0
+    );
   };
 
-  private hasGrpcOut = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
-
-      return (
-        selectAnd(appBoxChildren, [
-          { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
-          { prop: NodeAttr.grpcOut, op: '>', val: 0 }
-        ]).length > 0
-      );
-    }
+  private hasGrpcOut = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
 
-    return appBox.children().filter('[nodeType != "service"]').filter('[grpcOut > 0]').size() > 0;
+    return (
+      selectAnd(appBoxChildren, [
+        { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
+        { prop: NodeAttr.grpcOut, op: '>', val: 0 }
+      ]).length > 0
+    );
   };
 
-  private hasHttpTraffic = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
-      const notServices = select(appBoxChildren, { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE });
-
-      return (
-        selectOr(notServices, [
-          [{ prop: NodeAttr.httpIn, op: '>', val: 0 }],
-          [{ prop: NodeAttr.httpOut, op: '>', val: 0 }]
-        ]).length > 0
-      );
-    }
+  private hasHttpTraffic = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
+    const notServices = select(appBoxChildren, { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE });
 
-    return appBox.children().filter('[nodeType != "service"]').filter('[httpIn > 0],[httpOut > 0]').size() > 0;
+    return (
+      selectOr(notServices, [
+        [{ prop: NodeAttr.httpIn, op: '>', val: 0 }],
+        [{ prop: NodeAttr.httpOut, op: '>', val: 0 }]
+      ]).length > 0
+    );
   };
 
-  private hasHttpIn = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
+  private hasHttpIn = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
 
-      return (
-        selectAnd(appBoxChildren, [
-          { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
-          { prop: NodeAttr.httpIn, op: '>', val: 0 }
-        ]).length > 0
-      );
-    }
-
-    return appBox.children().filter('[nodeType != "service"]').filter('[httpIn > 0]').size() > 0;
+    return (
+      selectAnd(appBoxChildren, [
+        { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
+        { prop: NodeAttr.httpIn, op: '>', val: 0 }
+      ]).length > 0
+    );
   };
 
-  private hasHttpOut = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
+  private hasHttpOut = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
 
-      return (
-        selectAnd(appBoxChildren, [
-          { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
-          { prop: NodeAttr.httpOut, op: '>', val: 0 }
-        ]).length > 0
-      );
-    }
-
-    return appBox.children().filter('[nodeType != "service"]').filter('[httpOut > 0]').size() > 0;
+    return (
+      selectAnd(appBoxChildren, [
+        { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
+        { prop: NodeAttr.httpOut, op: '>', val: 0 }
+      ]).length > 0
+    );
   };
 
-  private hasTcpTraffic = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
-      const notServices = select(appBoxChildren, { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE });
-
-      return (
-        selectOr(notServices, [
-          [{ prop: NodeAttr.tcpIn, op: '>', val: 0 }],
-          [{ prop: NodeAttr.tcpOut, op: '>', val: 0 }]
-        ]).length > 0
-      );
-    }
+  private hasTcpTraffic = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
+    const notServices = select(appBoxChildren, { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE });
 
-    return appBox.children().filter('[nodeType != "service"]').filter('[tcpIn > 0],[tcpOut > 0]').size() > 0;
+    return (
+      selectOr(notServices, [[{ prop: NodeAttr.tcpIn, op: '>', val: 0 }], [{ prop: NodeAttr.tcpOut, op: '>', val: 0 }]])
+        .length > 0
+    );
   };
 
-  private hasTcpIn = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
+  private hasTcpIn = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
 
-      return (
-        selectAnd(appBoxChildren, [
-          { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
-          { prop: NodeAttr.tcpIn, op: '>', val: 0 }
-        ]).length > 0
-      );
-    }
-
-    return appBox.children().filter('[nodeType != "service"]').filter('[tcpIn > 0]').size() > 0;
+    return (
+      selectAnd(appBoxChildren, [
+        { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
+        { prop: NodeAttr.tcpIn, op: '>', val: 0 }
+      ]).length > 0
+    );
   };
 
-  private hasTcpOut = (appBox: any, isPF: boolean): boolean => {
-    if (isPF) {
-      const appBoxChildren = (appBox as Node).getAllNodeChildren();
+  private hasTcpOut = (appBox: any): boolean => {
+    const appBoxChildren = (appBox as Node).getAllNodeChildren();
 
-      return (
-        selectAnd(appBoxChildren, [
-          { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
-          { prop: NodeAttr.tcpOut, op: '>', val: 0 }
-        ]).length > 0
-      );
-    }
-
-    return appBox.children().filter('[nodeType != "service"]').filter('[tcpOut > 0]').size() > 0;
+    return (
+      selectAnd(appBoxChildren, [
+        { prop: NodeAttr.nodeType, op: '!=', val: NodeType.SERVICE },
+        { prop: NodeAttr.tcpOut, op: '>', val: 0 }
+      ]).length > 0
+    );
   };
 }
diff --git a/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx b/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx
index 32ce4013f..ae3aa50b0 100644
--- a/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelClusterBox.tsx
@@ -68,18 +68,13 @@ export class SummaryPanelClusterBox extends React.Component<SummaryPanelPropType
   }
 
   render(): React.ReactNode {
-    const isPF = !!this.props.data.isPF;
     const clusterBox = this.props.data.summaryTarget;
-    const data = isPF ? clusterBox.getData() : clusterBox.data();
-    const boxed = isPF ? descendents(clusterBox) : clusterBox.descendants();
+    const data = clusterBox.getData();
+    const boxed = descendents(clusterBox);
     const cluster = data[NodeAttr.cluster];
     const kialiInstances = serverConfig.clusters[cluster] ? serverConfig.clusters[cluster].kialiInstances : [];
 
-    let numSvc: number;
-    let numWorkloads: number;
-    let numEdges: number;
-
-    const { numApps, numVersions } = this.countApps(boxed, isPF);
+    const { numApps, numVersions } = this.countApps(boxed);
     const {
       grpcIn,
       grpcOut,
@@ -91,17 +86,11 @@ export class SummaryPanelClusterBox extends React.Component<SummaryPanelPropType
       tcpIn,
       tcpOut,
       tcpTotal
-    } = this.getBoxTraffic(boxed, isPF);
-
-    if (isPF) {
-      numSvc = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.SERVICE }).length;
-      numWorkloads = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.WORKLOAD }).length;
-      numEdges = edgesInOut(boxed).length;
-    } else {
-      numSvc = boxed.filter(`node[nodeType = "${NodeType.SERVICE}"]`).size();
-      numWorkloads = boxed.filter(`node[nodeType = "${NodeType.WORKLOAD}"]`).size();
-      numEdges = boxed.connectedEdges().size();
-    }
+    } = this.getBoxTraffic(boxed);
+
+    const numSvc = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.SERVICE }).length;
+    const numWorkloads = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.WORKLOAD }).length;
+    const numEdges = edgesInOut(boxed).length;
 
     const tooltipInboundRef = React.createRef();
     const tooltipOutboundRef = React.createRef();
@@ -243,8 +232,7 @@ export class SummaryPanelClusterBox extends React.Component<SummaryPanelPropType
   }
 
   private getBoxTraffic = (
-    boxed: any,
-    isPF: boolean
+    boxed: any
   ): {
     grpcIn: TrafficRateGrpc;
     grpcOut: TrafficRateGrpc;
@@ -258,73 +246,39 @@ export class SummaryPanelClusterBox extends React.Component<SummaryPanelPropType
     tcpTotal: TrafficRateTcp;
   } => {
     const clusterBox = this.props.data.summaryTarget;
-    const data = isPF ? clusterBox.getData() : clusterBox.data();
+    const data = clusterBox.getData();
     const cluster = data[NodeAttr.cluster];
 
     let inboundEdges: Edge[] | any;
     let outboundEdges: Edge[] | any;
     let totalEdges: Edge[] | any;
 
-    if (isPF) {
-      const controller = (clusterBox as Node).getController();
-      const { nodes } = elems(controller);
-      const outsideNodes = select(nodes, { prop: NodeAttr.cluster, op: '!=', val: cluster }) as Node[];
-      // inbound edges are from a different cluster
-      inboundEdges = edgesOut(outsideNodes, boxed);
-      // outbound edges are to a different different cluster
-      outboundEdges = edgesIn(outsideNodes, boxed);
-      // total edges are inbound + edges from boxed workload|app|root nodes (i.e. not injected service nodes or box nodes)
-      totalEdges = [...inboundEdges];
-      totalEdges.push(...edgesOut(select(boxed, { prop: NodeAttr.workload, op: 'truthy' }) as Node[]));
-    } else {
-      // inbound edges are from a different cluster
-      inboundEdges = clusterBox.cy().nodes(`[${NodeAttr.cluster} != "${cluster}"]`).edgesTo(boxed);
-      // outbound edges are to a different cluster
-      outboundEdges = boxed.edgesTo(`[${NodeAttr.cluster} != "${cluster}"]`);
-      // total edges are inbound + edges from boxed workload|app|root nodes (i.e. not injected service nodes or box nodes)
-      totalEdges = inboundEdges.add(boxed.filter(`[?${NodeAttr.workload}]`).edgesTo('*'));
-    }
+    const controller = (clusterBox as Node).getController();
+    const { nodes } = elems(controller);
+    const outsideNodes = select(nodes, { prop: NodeAttr.cluster, op: '!=', val: cluster }) as Node[];
+    // inbound edges are from a different cluster
+    inboundEdges = edgesOut(outsideNodes, boxed);
+    // outbound edges are to a different different cluster
+    outboundEdges = edgesIn(outsideNodes, boxed);
+    // total edges are inbound + edges from boxed workload|app|root nodes (i.e. not injected service nodes or box nodes)
+    totalEdges = [...inboundEdges];
+    totalEdges.push(...edgesOut(select(boxed, { prop: NodeAttr.workload, op: 'truthy' }) as Node[]));
 
     return {
-      grpcIn: getAccumulatedTrafficRateGrpc(inboundEdges, isPF),
-      grpcOut: getAccumulatedTrafficRateGrpc(outboundEdges, isPF),
-      grpcTotal: getAccumulatedTrafficRateGrpc(totalEdges, isPF),
-      httpIn: getAccumulatedTrafficRateHttp(inboundEdges, isPF),
-      httpOut: getAccumulatedTrafficRateHttp(outboundEdges, isPF),
-      httpTotal: getAccumulatedTrafficRateHttp(totalEdges, isPF),
+      grpcIn: getAccumulatedTrafficRateGrpc(inboundEdges),
+      grpcOut: getAccumulatedTrafficRateGrpc(outboundEdges),
+      grpcTotal: getAccumulatedTrafficRateGrpc(totalEdges),
+      httpIn: getAccumulatedTrafficRateHttp(inboundEdges),
+      httpOut: getAccumulatedTrafficRateHttp(outboundEdges),
+      httpTotal: getAccumulatedTrafficRateHttp(totalEdges),
       isGrpcRequests: this.props.trafficRates.includes(TrafficRate.GRPC_REQUEST),
-      tcpIn: getAccumulatedTrafficRateTcp(inboundEdges, isPF),
-      tcpOut: getAccumulatedTrafficRateTcp(outboundEdges, isPF),
-      tcpTotal: getAccumulatedTrafficRateTcp(totalEdges, isPF)
-    };
-  };
-
-  private countApps = (boxed: any, isPF: boolean): { numApps: number; numVersions: number } => {
-    if (isPF) {
-      return this.countAppsPF(boxed);
-    }
-
-    const appVersions: { [key: string]: Set<string> } = {};
-
-    boxed.filter(`node[nodeType = "${NodeType.APP}"]`).forEach((node: any) => {
-      const app = node.data(NodeAttr.app);
-
-      if (appVersions[app] === undefined) {
-        appVersions[app] = new Set();
-      }
-
-      appVersions[app].add(node.data(NodeAttr.version));
-    });
-
-    return {
-      numApps: Object.getOwnPropertyNames(appVersions).length,
-      numVersions: Object.getOwnPropertyNames(appVersions).reduce((totalCount: number, version: string) => {
-        return totalCount + appVersions[version].size;
-      }, 0)
+      tcpIn: getAccumulatedTrafficRateTcp(inboundEdges),
+      tcpOut: getAccumulatedTrafficRateTcp(outboundEdges),
+      tcpTotal: getAccumulatedTrafficRateTcp(totalEdges)
     };
   };
 
-  private countAppsPF = (boxed: any): { numApps: number; numVersions: number } => {
+  private countApps = (boxed: any): { numApps: number; numVersions: number } => {
     const appVersions: { [key: string]: Set<string> } = {};
 
     select(boxed, { prop: NodeAttr.nodeType, val: NodeType.APP }).forEach((node: GraphElement) => {
diff --git a/frontend/src/pages/Graph/SummaryPanelEdge.tsx b/frontend/src/pages/Graph/SummaryPanelEdge.tsx
index 7c9f7a12e..a7f05dee5 100644
--- a/frontend/src/pages/Graph/SummaryPanelEdge.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelEdge.tsx
@@ -29,7 +29,6 @@ import {
 } from './SummaryPanelCommon';
 import { Datapoint, IstioMetricsMap, Labels, Metric } from '../../types/Metrics';
 import { CancelablePromise, makeCancelablePromise } from '../../utils/CancelablePromises';
-import { decoratedEdgeData, decoratedNodeData } from '../../components/CytoscapeGraph/CytoscapeGraphUtils';
 import { ResponseFlagsTable } from 'components/SummaryPanel/ResponseFlagsTable';
 import { ResponseHostsTable } from 'components/SummaryPanel/ResponseHostsTable';
 import { KialiIcon } from 'config/KialiIcon';
@@ -159,15 +158,10 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
   };
 
   render(): React.ReactNode {
-    const isPF = !!this.props.data.isPF;
     const edge = this.props.data.summaryTarget;
-    const edgeData = isPF ? ((edge as Edge).getData() as DecoratedGraphEdgeData) : decoratedEdgeData(edge);
-    const sourceData = isPF
-      ? ((edge as Edge).getSource().getData() as DecoratedGraphNodeData)
-      : decoratedNodeData(edge.source());
-    const destData = isPF
-      ? ((edge as Edge).getTarget().getData() as DecoratedGraphNodeData)
-      : decoratedNodeData(edge.target());
+    const edgeData = (edge as Edge).getData() as DecoratedGraphEdgeData;
+    const sourceData = (edge as Edge).getSource().getData() as DecoratedGraphNodeData;
+    const destData = (edge as Edge).getTarget().getData() as DecoratedGraphNodeData;
     const mTLSPercentage = edgeData.isMTLS;
     const isMtls = mTLSPercentage && mTLSPercentage > 0;
     const hasPrincipals = !!edgeData.sourcePrincipal || !!edgeData.destPrincipal;
@@ -289,7 +283,7 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
                 </Tab>
               </SimpleTabs>
               {hr()}
-              {this.renderCharts(edge, isGrpc, isHttp, isTcp, isRequests, isPF)}
+              {this.renderCharts(edge, isGrpc, isHttp, isTcp, isRequests)}
             </div>
           )}
 
@@ -317,7 +311,7 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
 
               {hr()}
 
-              {this.renderCharts(edge, isGrpc, isHttp, isTcp, isRequests, isPF, isReversed)}
+              {this.renderCharts(edge, isGrpc, isHttp, isTcp, isRequests, isReversed)}
             </div>
           )}
 
@@ -431,15 +425,10 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
   };
 
   private updateCharts = (props: SummaryPanelPropType): void => {
-    const isPF = !!props.data.isPF;
     const edge = this.props.data.summaryTarget;
-    const edgeData = isPF ? ((edge as Edge).getData() as DecoratedGraphEdgeData) : decoratedEdgeData(edge);
-    const sourceData = isPF
-      ? ((edge as Edge).getSource().getData() as DecoratedGraphNodeData)
-      : decoratedNodeData(edge.source());
-    const destData = isPF
-      ? ((edge as Edge).getTarget().getData() as DecoratedGraphNodeData)
-      : decoratedNodeData(edge.target());
+    const edgeData = (edge as Edge).getData() as DecoratedGraphEdgeData;
+    const sourceData = (edge as Edge).getSource().getData() as DecoratedGraphNodeData;
+    const destData = (edge as Edge).getTarget().getData() as DecoratedGraphNodeData;
     const sourceMetricType = getNodeMetricType(sourceData);
     const destMetricType = getNodeMetricType(destData);
     const protocol = edgeData.protocol;
@@ -459,7 +448,7 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
     if (
       !destMetricType ||
       !sourceMetricType ||
-      !this.hasSupportedCharts(edge, isPF) ||
+      !this.hasSupportedCharts(edge) ||
       (!isGrpc && !isHttp && !isTcp) ||
       destData.isInaccessible
     ) {
@@ -713,10 +702,9 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
     isHttp: boolean,
     isTcp: boolean,
     isRequests: boolean,
-    isPF: boolean,
     isWaypointEdge?: boolean
   ): React.ReactNode => {
-    if (!this.hasSupportedCharts(edge, isPF)) {
+    if (!this.hasSupportedCharts(edge)) {
       return isGrpc || isHttp ? (
         <>
           <KialiIcon.Info /> Service graphs do not support service-to-service aggregate sparklines. See the chart above
@@ -731,7 +719,7 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
       );
     }
 
-    const destData = isPF ? (edge as Edge).getTarget().getData() : decoratedNodeData(edge.target());
+    const destData = (edge as Edge).getTarget().getData();
 
     if (destData.isInaccessible) {
       return (
@@ -817,9 +805,9 @@ export class SummaryPanelEdge extends React.Component<SummaryPanelPropType, Summ
     );
   };
 
-  private hasSupportedCharts = (edge: any, isPF: boolean): boolean => {
-    const sourceData = isPF ? (edge as Edge).getSource().getData() : decoratedNodeData(edge.source());
-    const destData = isPF ? (edge as Edge).getTarget().getData() : decoratedNodeData(edge.target());
+  private hasSupportedCharts = (edge: any): boolean => {
+    const sourceData = (edge as Edge).getSource().getData();
+    const destData = (edge as Edge).getTarget().getData();
     const sourceMetricType = getNodeMetricType(sourceData);
     const destMetricType = getNodeMetricType(destData);
 
diff --git a/frontend/src/pages/Graph/SummaryPanelGraph.tsx b/frontend/src/pages/Graph/SummaryPanelGraph.tsx
index 102749a67..a0e6c9bd1 100644
--- a/frontend/src/pages/Graph/SummaryPanelGraph.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelGraph.tsx
@@ -34,7 +34,7 @@ import { ValidationStatus } from 'types/IstioObjects';
 import { ValidationSummary } from 'components/Validations/ValidationSummary';
 import { ValidationSummaryLink } from '../../components/Link/ValidationSummaryLink';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
-import { NodeData } from 'pages/GraphPF/GraphPFElems';
+import { NodeData } from 'pages/Graph/GraphPFElems';
 import { edgesIn, edgesOut, elems, leafNodes, select } from 'helpers/GraphHelpers';
 import { SimpleTabs } from 'components/Tab/SimpleTabs';
 import { panelHeadingStyle, panelStyle } from './SummaryPanelStyle';
@@ -131,14 +131,12 @@ export class SummaryPanelGraph extends React.Component<SummaryPanelPropType, Sum
   };
 
   private graphTraffic?: SummaryPanelGraphTraffic;
-  private isPF = false;
   private metricsPromise?: CancelablePromise<ApiResponse<IstioMetricsMap>[]>;
   private validationSummaryPromises: PromisesRegistry = new PromisesRegistry();
 
   constructor(props: SummaryPanelPropType) {
     super(props);
 
-    this.isPF = !!props.data.isPF;
     this.state = { ...defaultState };
   }
 
@@ -197,40 +195,21 @@ export class SummaryPanelGraph extends React.Component<SummaryPanelPropType, Sum
       tcpOut: TrafficRateTcp,
       tcpTotal: TrafficRateTcp;
 
-    if (this.isPF) {
-      // PF Graph
-      const controller = this.props.data.summaryTarget as Visualization;
-
-      if (!controller) {
-        return null;
-      }
-      const { nodes, edges } = elems(controller);
-
-      numSvc = select(nodes, { prop: NodeAttr.nodeType, val: NodeType.SERVICE }).length;
-      numWorkloads = select(nodes, { prop: NodeAttr.nodeType, val: NodeType.WORKLOAD }).length;
-
-      ({ numApps, numVersions } = this.countApps());
-      numEdges = edges.length;
-
-      ({ grpcIn, grpcOut, grpcTotal, httpIn, httpOut, httpTotal, isGrpcRequests, tcpIn, tcpOut, tcpTotal } =
-        this.graphTraffic ?? this.getGraphTraffic());
-    } else {
-      // CY Graph
-      const cy = this.props.data.summaryTarget;
+    const controller = this.props.data.summaryTarget as Visualization;
 
-      if (!cy) {
-        return null;
-      }
+    if (!controller) {
+      return null;
+    }
+    const { nodes, edges } = elems(controller);
 
-      numSvc = cy.nodes(`[nodeType = "${NodeType.SERVICE}"]`).size();
-      numWorkloads = cy.nodes(`[nodeType = "${NodeType.WORKLOAD}"]`).size();
+    numSvc = select(nodes, { prop: NodeAttr.nodeType, val: NodeType.SERVICE }).length;
+    numWorkloads = select(nodes, { prop: NodeAttr.nodeType, val: NodeType.WORKLOAD }).length;
 
-      ({ numApps, numVersions } = this.countApps());
-      numEdges = cy.edges().size();
+    ({ numApps, numVersions } = this.countApps());
+    numEdges = edges.length;
 
-      ({ grpcIn, grpcOut, grpcTotal, httpIn, httpOut, httpTotal, isGrpcRequests, tcpIn, tcpOut, tcpTotal } =
-        this.graphTraffic ?? this.getGraphTraffic());
-    }
+    ({ grpcIn, grpcOut, grpcTotal, httpIn, httpOut, httpTotal, isGrpcRequests, tcpIn, tcpOut, tcpTotal } =
+      this.graphTraffic ?? this.getGraphTraffic());
 
     const tooltipInboundRef = React.createRef();
     const tooltipOutboundRef = React.createRef();
@@ -381,34 +360,6 @@ export class SummaryPanelGraph extends React.Component<SummaryPanelPropType, Sum
   }
 
   private getGraphTraffic = (): SummaryPanelGraphTraffic => {
-    if (this.isPF) {
-      return this.getGraphTrafficPF();
-    }
-
-    // when getting total traffic rates don't count requests from injected service nodes
-    const cy = this.props.data.summaryTarget;
-    const totalEdges = cy.nodes(`[nodeType != "${NodeType.SERVICE}"][!isBox]`).edgesTo('*');
-    const inboundEdges = cy.nodes(`[?${NodeAttr.isRoot}]`).edgesTo('*');
-    const outboundEdges = cy
-      .nodes()
-      .leaves(`node[?${NodeAttr.isOutside}],[?${NodeAttr.isServiceEntry}]`)
-      .connectedEdges();
-
-    return {
-      grpcIn: getAccumulatedTrafficRateGrpc(inboundEdges),
-      grpcOut: getAccumulatedTrafficRateGrpc(outboundEdges),
-      grpcTotal: getAccumulatedTrafficRateGrpc(totalEdges),
-      httpIn: getAccumulatedTrafficRateHttp(inboundEdges),
-      httpOut: getAccumulatedTrafficRateHttp(outboundEdges),
-      httpTotal: getAccumulatedTrafficRateHttp(totalEdges),
-      isGrpcRequests: this.props.trafficRates.includes(TrafficRate.GRPC_REQUEST),
-      tcpIn: getAccumulatedTrafficRateTcp(inboundEdges),
-      tcpOut: getAccumulatedTrafficRateTcp(outboundEdges),
-      tcpTotal: getAccumulatedTrafficRateTcp(totalEdges)
-    };
-  };
-
-  private getGraphTrafficPF = (): SummaryPanelGraphTraffic => {
     const controller = this.props.data.summaryTarget as Visualization;
     const { nodes } = elems(controller);
 
@@ -426,46 +377,20 @@ export class SummaryPanelGraph extends React.Component<SummaryPanelPropType, Sum
     );
 
     return {
-      grpcIn: getAccumulatedTrafficRateGrpc(inboundEdges, true),
-      grpcOut: getAccumulatedTrafficRateGrpc(outboundEdges, true),
-      grpcTotal: getAccumulatedTrafficRateGrpc(totalEdges, true),
-      httpIn: getAccumulatedTrafficRateHttp(inboundEdges, true),
-      httpOut: getAccumulatedTrafficRateHttp(outboundEdges, true),
-      httpTotal: getAccumulatedTrafficRateHttp(totalEdges, true),
+      grpcIn: getAccumulatedTrafficRateGrpc(inboundEdges),
+      grpcOut: getAccumulatedTrafficRateGrpc(outboundEdges),
+      grpcTotal: getAccumulatedTrafficRateGrpc(totalEdges),
+      httpIn: getAccumulatedTrafficRateHttp(inboundEdges),
+      httpOut: getAccumulatedTrafficRateHttp(outboundEdges),
+      httpTotal: getAccumulatedTrafficRateHttp(totalEdges),
       isGrpcRequests: this.props.trafficRates.includes(TrafficRate.GRPC_REQUEST),
-      tcpIn: getAccumulatedTrafficRateTcp(inboundEdges, true),
-      tcpOut: getAccumulatedTrafficRateTcp(outboundEdges, true),
-      tcpTotal: getAccumulatedTrafficRateTcp(totalEdges, true)
+      tcpIn: getAccumulatedTrafficRateTcp(inboundEdges),
+      tcpOut: getAccumulatedTrafficRateTcp(outboundEdges),
+      tcpTotal: getAccumulatedTrafficRateTcp(totalEdges)
     };
   };
 
   private countApps = (): { numApps: number; numVersions: number } => {
-    if (this.isPF) {
-      return this.countAppsPF();
-    }
-
-    const cy = this.props.data.summaryTarget;
-    const appVersions: { [key: string]: Set<string> } = {};
-
-    cy.$(`node[nodeType = "${NodeType.APP}"]`).forEach(node => {
-      const app = node.data(NodeAttr.app);
-
-      if (appVersions[app] === undefined) {
-        appVersions[app] = new Set();
-      }
-
-      appVersions[app].add(node.data(NodeAttr.version));
-    });
-
-    return {
-      numApps: Object.getOwnPropertyNames(appVersions).length,
-      numVersions: Object.getOwnPropertyNames(appVersions).reduce((totalCount: number, version: string) => {
-        return totalCount + appVersions[version].size;
-      }, 0)
-    };
-  };
-
-  private countAppsPF = (): { numApps: number; numVersions: number } => {
     const controller = this.props.data.summaryTarget as Visualization;
     const { nodes } = elems(controller);
 
diff --git a/frontend/src/pages/Graph/SummaryPanelNamespaceBox.tsx b/frontend/src/pages/Graph/SummaryPanelNamespaceBox.tsx
index eae38024d..7891f4d29 100644
--- a/frontend/src/pages/Graph/SummaryPanelNamespaceBox.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelNamespaceBox.tsx
@@ -148,15 +148,15 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
 
   componentDidMount(): void {
     this.boxTraffic = this.getBoxTraffic();
-    this.updateCharts(!!this.props.data.isPF);
-    this.updateValidation(!!this.props.data.isPF);
+    this.updateCharts();
+    this.updateValidation();
   }
 
   componentDidUpdate(prevProps: SummaryPanelPropType): void {
     if (shouldRefreshData(prevProps, this.props)) {
       this.boxTraffic = this.getBoxTraffic();
-      this.updateCharts(!!this.props.data.isPF);
-      this.updateValidation(!!this.props.data.isPF);
+      this.updateCharts();
+      this.updateValidation();
     }
   }
 
@@ -170,29 +170,18 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
   }
 
   render(): React.ReactNode {
-    const isPF = !!this.props.data.isPF;
     const namespaceBox = this.props.data.summaryTarget;
-    const data = isPF ? namespaceBox.getData() : namespaceBox.data();
-    const boxed = isPF ? descendents(namespaceBox) : namespaceBox.descendants();
+    const data = namespaceBox.getData();
+    const boxed = descendents(namespaceBox);
     const namespace = data[NodeAttr.namespace];
 
-    let numSvc: number;
-    let numWorkloads: number;
-    let numEdges: number;
-
-    const { numApps, numVersions } = this.countApps(boxed, isPF);
+    const { numApps, numVersions } = this.countApps(boxed);
     const { grpcIn, grpcOut, grpcTotal, httpIn, httpOut, httpTotal, isGrpcRequests, tcpIn, tcpOut, tcpTotal } =
       this.boxTraffic ?? this.getBoxTraffic();
 
-    if (isPF) {
-      numSvc = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.SERVICE }).length;
-      numWorkloads = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.WORKLOAD }).length;
-      numEdges = edgesInOut(boxed).length;
-    } else {
-      numSvc = boxed.filter(`node[nodeType = "${NodeType.SERVICE}"]`).size();
-      numWorkloads = boxed.filter(`node[nodeType = "${NodeType.WORKLOAD}"]`).size();
-      numEdges = boxed.connectedEdges().size();
-    }
+    const numSvc = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.SERVICE }).length;
+    const numWorkloads = select(boxed, { prop: NodeAttr.nodeType, val: NodeType.WORKLOAD }).length;
+    const numEdges = edgesInOut(boxed).length;
 
     const tooltipInboundRef = React.createRef();
     const tooltipOutboundRef = React.createRef();
@@ -333,7 +322,7 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
 
                 <div>
                   {hr()}
-                  {this.renderCharts(isPF)}
+                  {this.renderCharts()}
                 </div>
               </div>
             </Tab>
@@ -344,10 +333,9 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
   }
 
   private getBoxTraffic = (): SummaryPanelNamespaceBoxTraffic => {
-    const isPF = !!this.props.data.isPF;
     const namespaceBox = this.props.data.summaryTarget;
-    const data = isPF ? namespaceBox.getData() : namespaceBox.data();
-    const boxed = isPF ? descendents(namespaceBox) : namespaceBox.descendants();
+    const data = namespaceBox.getData();
+    const boxed = descendents(namespaceBox);
     const namespace = data[NodeAttr.namespace];
     const cluster = data[NodeAttr.cluster];
 
@@ -355,78 +343,39 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
     let outboundEdges: Edge[] | any;
     let totalEdges: Edge[] | any;
 
-    if (isPF) {
-      const controller = (namespaceBox as Node).getController();
-      const { nodes } = elems(controller);
+    const controller = (namespaceBox as Node).getController();
+    const { nodes } = elems(controller);
 
-      const outsideNodes = selectOr(nodes, [
-        [{ prop: NodeAttr.namespace, op: '!=', val: namespace }],
-        [{ prop: NodeAttr.cluster, op: '!=', val: cluster }]
-      ]) as Node[];
+    const outsideNodes = selectOr(nodes, [
+      [{ prop: NodeAttr.namespace, op: '!=', val: namespace }],
+      [{ prop: NodeAttr.cluster, op: '!=', val: cluster }]
+    ]) as Node[];
 
-      // inbound edges are from a different namespace or a different cluster
-      inboundEdges = edgesOut(outsideNodes, boxed);
+    // inbound edges are from a different namespace or a different cluster
+    inboundEdges = edgesOut(outsideNodes, boxed);
 
-      // outbound edges are to a different namespace or a different cluster
-      outboundEdges = edgesIn(outsideNodes, boxed);
+    // outbound edges are to a different namespace or a different cluster
+    outboundEdges = edgesIn(outsideNodes, boxed);
 
-      // total edges are inbound + edges from boxed workload|app|root nodes (i.e. not injected service nodes or box nodes)
-      totalEdges = [...inboundEdges];
-      totalEdges.push(...edgesOut(select(boxed, { prop: NodeAttr.workload, op: 'truthy' }) as Node[]));
-    } else {
-      // inbound edges are from a different namespace or a different cluster
-      inboundEdges = namespaceBox
-        .cy()
-        .nodes(`[${NodeAttr.namespace} != "${namespace}"],[${NodeAttr.cluster} != "${cluster}"]`)
-        .edgesTo(boxed);
-
-      // outbound edges are to a different namespace or a different cluster
-      outboundEdges = boxed.edgesTo(`[${NodeAttr.namespace} != "${namespace}"],[${NodeAttr.cluster} != "${cluster}"]`);
-
-      // total edges are inbound + edges from boxed workload|app|root nodes (i.e. not injected service nodes or box nodes)
-      totalEdges = inboundEdges.add(boxed.filter(`[?${NodeAttr.workload}]`).edgesTo('*'));
-    }
+    // total edges are inbound + edges from boxed workload|app|root nodes (i.e. not injected service nodes or box nodes)
+    totalEdges = [...inboundEdges];
+    totalEdges.push(...edgesOut(select(boxed, { prop: NodeAttr.workload, op: 'truthy' }) as Node[]));
 
     return {
-      grpcIn: getAccumulatedTrafficRateGrpc(inboundEdges, isPF),
-      grpcOut: getAccumulatedTrafficRateGrpc(outboundEdges, isPF),
-      grpcTotal: getAccumulatedTrafficRateGrpc(totalEdges, isPF),
-      httpIn: getAccumulatedTrafficRateHttp(inboundEdges, isPF),
-      httpOut: getAccumulatedTrafficRateHttp(outboundEdges, isPF),
-      httpTotal: getAccumulatedTrafficRateHttp(totalEdges, isPF),
+      grpcIn: getAccumulatedTrafficRateGrpc(inboundEdges),
+      grpcOut: getAccumulatedTrafficRateGrpc(outboundEdges),
+      grpcTotal: getAccumulatedTrafficRateGrpc(totalEdges),
+      httpIn: getAccumulatedTrafficRateHttp(inboundEdges),
+      httpOut: getAccumulatedTrafficRateHttp(outboundEdges),
+      httpTotal: getAccumulatedTrafficRateHttp(totalEdges),
       isGrpcRequests: this.props.trafficRates.includes(TrafficRate.GRPC_REQUEST),
-      tcpIn: getAccumulatedTrafficRateTcp(inboundEdges, isPF),
-      tcpOut: getAccumulatedTrafficRateTcp(outboundEdges, isPF),
-      tcpTotal: getAccumulatedTrafficRateTcp(totalEdges, isPF)
+      tcpIn: getAccumulatedTrafficRateTcp(inboundEdges),
+      tcpOut: getAccumulatedTrafficRateTcp(outboundEdges),
+      tcpTotal: getAccumulatedTrafficRateTcp(totalEdges)
     };
   };
 
-  private countApps = (boxed: any, isPF: boolean): { numApps: number; numVersions: number } => {
-    if (isPF) {
-      return this.countAppsPF(boxed);
-    }
-
-    const appVersions: { [key: string]: Set<string> } = {};
-
-    boxed.filter(`node[nodeType = "${NodeType.APP}"]`).forEach((node: any) => {
-      const app = node.data(NodeAttr.app);
-
-      if (appVersions[app] === undefined) {
-        appVersions[app] = new Set();
-      }
-
-      appVersions[app].add(node.data(NodeAttr.version));
-    });
-
-    return {
-      numApps: Object.getOwnPropertyNames(appVersions).length,
-      numVersions: Object.getOwnPropertyNames(appVersions).reduce((totalCount: number, version: string) => {
-        return totalCount + appVersions[version].size;
-      }, 0)
-    };
-  };
-
-  private countAppsPF = (boxed: Node[]): { numApps: number; numVersions: number } => {
+  private countApps = (boxed: any): { numApps: number; numVersions: number } => {
     const appVersions: { [key: string]: Set<string> } = {};
 
     select(boxed, { prop: NodeAttr.nodeType, val: NodeType.APP }).forEach((node: GraphElement) => {
@@ -516,12 +465,10 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
     </>
   );
 
-  private renderCharts = (isPF: boolean): React.ReactNode => {
+  private renderCharts = (): React.ReactNode => {
     const props: SummaryPanelPropType = this.props;
 
-    const namespace = isPF
-      ? props.data.summaryTarget.getData()[NodeAttr.namespace]
-      : props.data.summaryTarget.data(NodeAttr.namespace);
+    const namespace = props.data.summaryTarget.getData()[NodeAttr.namespace];
 
     if (this.state.loading) {
       return <strong>Loading chart...</strong>;
@@ -615,16 +562,11 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
     );
   };
 
-  private updateCharts = (isPF: boolean): void => {
+  private updateCharts = (): void => {
     const props: SummaryPanelPropType = this.props;
 
-    const cluster = isPF
-      ? props.data.summaryTarget.getData()[NodeAttr.cluster]
-      : props.data.summaryTarget.data(NodeAttr.cluster);
-
-    const namespace = isPF
-      ? props.data.summaryTarget.getData()[NodeAttr.namespace]
-      : props.data.summaryTarget.data(NodeAttr.namespace);
+    const cluster = props.data.summaryTarget.getData()[NodeAttr.cluster];
+    const namespace = props.data.summaryTarget.getData()[NodeAttr.namespace];
 
     if (namespace === UNKNOWN) {
       this.setState({
@@ -744,10 +686,8 @@ export class SummaryPanelNamespaceBox extends React.Component<SummaryPanelPropTy
     this.setState({ loading: true, metricsLoadError: null });
   };
 
-  private updateValidation = (isPF: boolean): void => {
-    const namespace = isPF
-      ? this.props.data.summaryTarget.getData()[NodeAttr.namespace]
-      : this.props.data.summaryTarget.data(NodeAttr.namespace);
+  private updateValidation = (): void => {
+    const namespace = this.props.data.summaryTarget.getData()[NodeAttr.namespace];
 
     this.validationPromise = makeCancelablePromise(API.getNamespaceValidations(namespace));
 
diff --git a/frontend/src/pages/Graph/SummaryPanelNode.tsx b/frontend/src/pages/Graph/SummaryPanelNode.tsx
index 9e2dca1bc..173207c07 100644
--- a/frontend/src/pages/Graph/SummaryPanelNode.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelNode.tsx
@@ -9,9 +9,8 @@ import {
 } from './SummaryLink';
 import { DecoratedGraphNodeData, DestService, NodeType, RankResult, SummaryPanelPropType } from '../../types/Graph';
 import { getTitle, summaryBodyTabs, summaryFont, summaryPanel } from './SummaryPanelCommon';
-import { decoratedNodeData } from '../../components/CytoscapeGraph/CytoscapeGraphUtils';
 import { KialiIcon } from 'config/KialiIcon';
-import { clickHandler, getOptions } from 'components/CytoscapeGraph/ContextMenu/NodeContextMenu';
+import { clickHandler, getOptions } from 'pages/Graph/ContextMenu/NodeContextMenu';
 import {
   Dropdown,
   DropdownGroup,
@@ -123,7 +122,7 @@ export class SummaryPanelNodeComponent extends React.Component<SummaryPanelNodeC
 
   render(): React.ReactNode {
     const node = this.props.data.summaryTarget;
-    const nodeData = this.props.data.isPF ? node.getData() : decoratedNodeData(node);
+    const nodeData = node.getData();
     const { nodeType, app, service, workload, isServiceEntry } = nodeData;
     const servicesList = nodeType !== NodeType.SERVICE && renderDestServicesLinks(nodeData);
     const destsList = nodeType === NodeType.SERVICE && isServiceEntry && this.renderDestServices(nodeData);
@@ -546,7 +545,7 @@ export class SummaryPanelNodeComponent extends React.Component<SummaryPanelNodeC
 
     if (this.props.onLaunchWizard) {
       const node = this.props.data.summaryTarget;
-      const nodeData = this.props.data.isPF ? node.getData() : decoratedNodeData(node);
+      const nodeData = node.getData();
 
       this.props.onLaunchWizard(
         key,
@@ -578,7 +577,7 @@ export const SummaryPanelNode: React.FC<SummaryPanelNodeProps> = (props: Summary
   const [isKebabOpen, setIsKebabOpen] = React.useState<boolean>(false);
 
   const node = props.data.summaryTarget;
-  const nodeData = props.data.isPF ? node.getData() : decoratedNodeData(node);
+  const nodeData = node.getData();
 
   const [serviceDetails, gateways, peerAuthentications, isServiceDetailsLoading] = useServiceDetailForGraphNode(
     nodeData,
diff --git a/frontend/src/pages/Graph/SummaryPanelNodeTraffic.tsx b/frontend/src/pages/Graph/SummaryPanelNodeTraffic.tsx
index 5356621d2..897a86a5a 100644
--- a/frontend/src/pages/Graph/SummaryPanelNodeTraffic.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelNodeTraffic.tsx
@@ -30,9 +30,8 @@ import {
 } from './SummaryPanelCommon';
 import { CancelablePromise, makeCancelablePromise } from '../../utils/CancelablePromises';
 import { Reporter } from '../../types/MetricsOptions';
-import { decoratedNodeData } from '../../components/CytoscapeGraph/CytoscapeGraphUtils';
 import { KialiIcon } from 'config/KialiIcon';
-import { edgesOut, nodesIn, select } from 'helpers/GraphHelpers';
+import { edgesOut, nodesIn, nodesOut, select } from 'helpers/GraphHelpers';
 import { ApiResponse } from 'types/Api';
 import { serverConfig } from 'config';
 
@@ -127,9 +126,8 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
   }
 
   updateCharts(props: SummaryPanelNodeProps): void {
-    const isPF = !!props.data.isPF;
     const node = props.data.summaryTarget;
-    const nodeData = isPF ? (node.getData() as DecoratedGraphNodeData) : decoratedNodeData(node);
+    const nodeData = node.getData();
     const nodeMetricType = getNodeMetricType(nodeData);
     const isGrpcRequests = this.isGrpcRequests();
 
@@ -377,9 +375,8 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
   };
 
   render(): React.ReactNode {
-    const isPF = !!this.props.data.isPF;
     const node = this.props.data.summaryTarget;
-    const nodeData = isPF ? node.getData() : decoratedNodeData(node);
+    const nodeData = node.getData();
     const hasGrpc = this.hasGrpcTraffic(nodeData);
     const hasGrpcIn = hasGrpc && this.hasGrpcIn(nodeData);
     const hasGrpcOut = hasGrpc && this.hasGrpcOut(nodeData);
@@ -394,20 +391,20 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
       <>
         {hasGrpc && this.isGrpcRequests() && (
           <>
-            {this.renderGrpcRates(node, isPF)}
+            {this.renderGrpcRates(node)}
             {hr()}
           </>
         )}
 
         {hasHttp && (
           <>
-            {this.renderHttpRates(node, isPF)}
+            {this.renderHttpRates(node)}
             {hr()}
           </>
         )}
 
         <div>
-          {this.renderSparklines(node, isPF)}
+          {this.renderSparklines(node)}
           {hr()}
         </div>
 
@@ -426,9 +423,9 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
     );
   }
 
-  private renderGrpcRates = (node: any, isPF: boolean): React.ReactNode => {
-    const inbound = getTrafficRateGrpc(node, isPF);
-    const outboundEdges = isPF ? edgesOut([node]) : node.edgesTo('*');
+  private renderGrpcRates = (node: any): React.ReactNode => {
+    const inbound = getTrafficRateGrpc(node);
+    const outboundEdges = edgesOut([node]);
     const outbound = getAccumulatedTrafficRateGrpc(outboundEdges);
 
     return (
@@ -444,10 +441,10 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
     );
   };
 
-  private renderHttpRates = (node: any, isPF: boolean): React.ReactNode => {
-    const inbound = getTrafficRateHttp(node, isPF);
-    const outboundEdges = isPF ? edgesOut([node]) : node.edgesTo('*');
-    const outbound = getAccumulatedTrafficRateHttp(outboundEdges, isPF);
+  private renderHttpRates = (node: any): React.ReactNode => {
+    const inbound = getTrafficRateHttp(node);
+    const outboundEdges = edgesOut([node]);
+    const outbound = getAccumulatedTrafficRateHttp(outboundEdges);
 
     return (
       <InOutRateTableHttp
@@ -466,8 +463,8 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
     );
   };
 
-  private renderSparklines = (node: any, isPF: boolean): React.ReactNode => {
-    const nodeData = isPF ? node.getData() : decoratedNodeData(node);
+  private renderSparklines = (node: any): React.ReactNode => {
+    const nodeData = node.getData();
 
     if (NodeType.UNKNOWN === nodeData.nodeType) {
       return (
@@ -507,9 +504,7 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
     let serviceWithUnknownSource = false;
 
     if (isServiceNode) {
-      serviceWithUnknownSource = isPF
-        ? select(nodesIn([node]), { prop: NodeAttr.nodeType, val: NodeType.UNKNOWN }).length > 0
-        : (serviceWithUnknownSource = node.incomers(`node[${NodeAttr.nodeType} = "${NodeType.UNKNOWN}"]`).size() > 0);
+      serviceWithUnknownSource = select(nodesIn([node]), { prop: NodeAttr.nodeType, val: NodeType.UNKNOWN }).length > 0;
     }
 
     let grpcCharts: React.ReactNode, httpCharts: React.ReactNode, tcpCharts: React.ReactNode;
@@ -541,7 +536,7 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
                 hide={isInOutSameNode}
               />
 
-              {!isPF && this.isIstioOutboundCornerCase(node) && (
+              {this.isIstioOutboundCornerCase(node) && (
                 <div>
                   <KialiIcon.Info /> Traffic to Istio namespaces not included. Use edge for details.
                 </div>
@@ -600,7 +595,7 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
                 hide={isInOutSameNode}
               />
 
-              {!isPF && this.isIstioOutboundCornerCase(node) && (
+              {this.isIstioOutboundCornerCase(node) && (
                 <div>
                   <KialiIcon.Info />" Traffic to Istio namespaces not included. Use edge for details.
                 </div>
@@ -657,15 +652,14 @@ export class SummaryPanelNodeTraffic extends React.Component<SummaryPanelNodePro
 
   // We need to handle the special case of a non-istio, non-unknown node with outbound traffic to istio.
   // The traffic is lost because it is dest-only and we use source-reporting.
-  // TODO: Currently not called for PFT
   private isIstioOutboundCornerCase = (node: any): boolean => {
-    const nodeData = decoratedNodeData(node);
+    const nodeData = node.getData();
 
     if (nodeData.nodeType === NodeType.UNKNOWN || nodeData.isIstio) {
       return false;
     }
 
-    return node.edgesTo(`node[?${NodeAttr.isIstio}]`).size() > 0;
+    return nodesOut([node]).filter(n => (n.getData() as DecoratedGraphNodeData).isIstio).length > 0;
   };
 
   private hasGrpcTraffic = (data: DecoratedGraphNodeData): boolean => {
diff --git a/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx b/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx
index 94d45e380..a31404175 100644
--- a/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx
+++ b/frontend/src/pages/Graph/SummaryPanelTraceDetails.tsx
@@ -11,8 +11,6 @@ import { TracingThunkActions } from 'actions/TracingThunkActions';
 import { GraphActions } from 'actions/GraphActions';
 import { PFColors } from 'components/Pf/PfColors';
 import { findChildren, findParent, formatDuration } from 'utils/tracing/TracingHelper';
-import { decoratedNodeData } from 'components/CytoscapeGraph/CytoscapeGraphUtils';
-import { FocusAnimation } from 'components/CytoscapeGraph/FocusAnimation';
 import { FormattedTraceInfo, shortIDStyle } from 'components/TracingIntegration/TracingResults/FormattedTraceInfo';
 import { SimpleSelect } from 'components/Select/SimpleSelect';
 import { summaryFont, summaryTitle } from './SummaryPanelCommon';
@@ -23,8 +21,7 @@ import { responseFlags } from 'utils/ResponseFlags';
 import { isParentKiosk, kioskContextMenuAction } from '../../components/Kiosk/KioskActions';
 import { Visualization, Node } from '@patternfly/react-topology';
 import { elems, selectAnd } from 'helpers/GraphHelpers';
-import { FocusNode } from 'pages/GraphPF/GraphPF';
-import { GraphSelectorBuilder } from './GraphSelector';
+import { FocusNode } from 'pages/Graph/GraphPF';
 import { ExternalServiceInfo } from '../../types/StatusState';
 import { isMultiCluster } from '../../config';
 import { KialiIcon } from 'config/KialiIcon';
@@ -99,13 +96,12 @@ class SummaryPanelTraceDetailsComponent extends React.Component<Props, State> {
   }
 
   render(): React.ReactNode {
-    const isPF = this.props.data.isPF;
     let node: any = {};
     let nodeData: any = {};
 
     if (this.props.data.summaryType === 'node') {
       node = this.props.data.summaryTarget;
-      nodeData = isPF ? node.getData() : decoratedNodeData(node);
+      nodeData = node.getData();
     }
 
     const tracesDetailsURL = nodeData.namespace
@@ -133,7 +129,7 @@ class SummaryPanelTraceDetailsComponent extends React.Component<Props, State> {
       </span>
     );
 
-    const spans: RichSpanData[] = (isPF ? nodeData['hasSpans'] : nodeData['spans']) || [];
+    const spans: RichSpanData[] = nodeData['hasSpans'] ?? [];
     let currentSpan = spans.find(s => s.spanID === this.state.selectedSpanID);
 
     if (!currentSpan && spans.length > 0) {
@@ -236,7 +232,7 @@ class SummaryPanelTraceDetailsComponent extends React.Component<Props, State> {
 
   private spanViewLink(span: RichSpanData): string | undefined {
     const node = this.props.data.summaryTarget;
-    const nodeData = this.props.data.isPF ? node.getData() : decoratedNodeData(node);
+    const nodeData = node.getData();
 
     return nodeData.namespace
       ? `/namespaces/${nodeData.namespace}${
@@ -312,64 +308,7 @@ class SummaryPanelTraceDetailsComponent extends React.Component<Props, State> {
   }
 
   private linkToSpan(current: RichSpanData, target: RichSpanData, text: string): React.ReactNode {
-    if (this.props.data.isPF) {
-      return this.linkToSpanPF(current, target, text);
-    }
-
-    const useApp = this.props.graphType === GraphType.APP || this.props.graphType === GraphType.SERVICE;
-    const currentElt = useApp ? current.app : current.workload;
-    const targetElt = useApp ? target.app : target.workload;
-
-    let tooltipContent = <>{text}</>;
-
-    if (targetElt) {
-      const cy = this.props.data.summaryTarget.cy();
-      const selBuilder = new GraphSelectorBuilder().namespace(target.namespace).class('span');
-      const selector = useApp ? selBuilder.app(targetElt).build() : selBuilder.workload(targetElt).build();
-
-      tooltipContent = (
-        <>
-          <Button
-            variant={ButtonVariant.link}
-            style={{ marginRight: '0.25rem' }}
-            onClick={() => {
-              this.setState({ selectedSpanID: target.spanID });
-
-              if (targetElt !== currentElt || target.namespace !== current.namespace) {
-                cy.elements(selector).trigger('tap');
-              }
-            }}
-            isInline
-          >
-            <span style={summaryFont}>{text}</span>
-          </Button>
-
-          <Button
-            variant={ButtonVariant.link}
-            onClick={() => new FocusAnimation(cy).start(cy.elements(selector))}
-            isInline
-          >
-            <span style={summaryFont}>
-              <KialiIcon.MapMarker />
-            </span>
-          </Button>
-        </>
-      );
-    }
-
-    return (
-      <Tooltip
-        key={target.spanID}
-        content={
-          <>
-            <div>Operation name: {target.operationName}</div>
-            <div>Workload: {target.workload ?? 'unknown'}</div>
-          </>
-        }
-      >
-        {tooltipContent}
-      </Tooltip>
-    );
+    return this.linkToSpanPF(current, target, text);
   }
 
   private linkToSpanPF(current: RichSpanData, target: RichSpanData, text: string): React.ReactNode {
diff --git a/frontend/src/pages/GraphPF/TracePF.ts b/frontend/src/pages/Graph/TracePF.ts
similarity index 100%
rename from frontend/src/pages/GraphPF/TracePF.ts
rename to frontend/src/pages/Graph/TracePF.ts
diff --git a/frontend/src/pages/GraphPF/TrafficAnimation/AnimationEdge.tsx b/frontend/src/pages/Graph/TrafficAnimation/AnimationEdge.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/TrafficAnimation/AnimationEdge.tsx
rename to frontend/src/pages/Graph/TrafficAnimation/AnimationEdge.tsx
diff --git a/frontend/src/pages/GraphPF/TrafficAnimation/AnimationTimerConfig.ts b/frontend/src/pages/Graph/TrafficAnimation/AnimationTimerConfig.ts
similarity index 100%
rename from frontend/src/pages/GraphPF/TrafficAnimation/AnimationTimerConfig.ts
rename to frontend/src/pages/Graph/TrafficAnimation/AnimationTimerConfig.ts
diff --git a/frontend/src/pages/GraphPF/TrafficAnimation/TrafficPointRendererPF.tsx b/frontend/src/pages/Graph/TrafficAnimation/TrafficPointRendererPF.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/TrafficAnimation/TrafficPointRendererPF.tsx
rename to frontend/src/pages/Graph/TrafficAnimation/TrafficPointRendererPF.tsx
diff --git a/frontend/src/pages/GraphPF/TrafficAnimation/TrafficRendererPF.tsx b/frontend/src/pages/Graph/TrafficAnimation/TrafficRendererPF.tsx
similarity index 99%
rename from frontend/src/pages/GraphPF/TrafficAnimation/TrafficRendererPF.tsx
rename to frontend/src/pages/Graph/TrafficAnimation/TrafficRendererPF.tsx
index 8de6a31d8..76b96d0cc 100644
--- a/frontend/src/pages/GraphPF/TrafficAnimation/TrafficRendererPF.tsx
+++ b/frontend/src/pages/Graph/TrafficAnimation/TrafficRendererPF.tsx
@@ -7,7 +7,7 @@ import {
 import { Protocol } from '../../../types/Graph';
 import { timerConfig, tcpTimerConfig } from './AnimationTimerConfig';
 import { Controller, Edge, EdgeAnimationSpeed, EdgeStyle } from '@patternfly/react-topology';
-import { EdgeData } from 'pages/GraphPF/GraphPFElems';
+import { EdgeData } from 'pages/Graph/GraphPFElems';
 import { PFColors } from 'components/Pf/PfColors';
 import { setObserved } from 'helpers/GraphHelpers';
 import { serverConfig } from 'config';
diff --git a/frontend/src/components/CytoscapeGraph/__tests__/GraphScore.test.ts b/frontend/src/pages/Graph/__tests__/GraphScore.test.ts
similarity index 100%
rename from frontend/src/components/CytoscapeGraph/__tests__/GraphScore.test.ts
rename to frontend/src/pages/Graph/__tests__/GraphScore.test.ts
diff --git a/frontend/src/pages/Graph/__tests__/SummaryPanelNode.test.tsx b/frontend/src/pages/Graph/__tests__/SummaryPanelNode.test.tsx
index be6f7743f..3f873ac86 100644
--- a/frontend/src/pages/Graph/__tests__/SummaryPanelNode.test.tsx
+++ b/frontend/src/pages/Graph/__tests__/SummaryPanelNode.test.tsx
@@ -22,7 +22,10 @@ describe('SummaryPanelNodeComponent', () => {
       destServices: []
     };
     const target = {
-      data: (destServices?) => (destServices ? [] : nodeData)
+      data: (destServices?) => (destServices ? [] : nodeData),
+      getData: () => {
+        return nodeData;
+      }
     };
     defaultProps = {
       tracingState: {},
diff --git a/frontend/src/pages/GraphPF/components/stylesComponentFactory.tsx b/frontend/src/pages/Graph/components/stylesComponentFactory.tsx
similarity index 83%
rename from frontend/src/pages/GraphPF/components/stylesComponentFactory.tsx
rename to frontend/src/pages/Graph/components/stylesComponentFactory.tsx
index 92631a2b6..0e5a93146 100644
--- a/frontend/src/pages/GraphPF/components/stylesComponentFactory.tsx
+++ b/frontend/src/pages/Graph/components/stylesComponentFactory.tsx
@@ -11,7 +11,7 @@ import {
   withSelection
 } from '@patternfly/react-topology';
 import { ExternalLinkAltIcon } from '@patternfly/react-icons';
-import { clickHandler, ContextMenuOption, getOptions } from 'components/CytoscapeGraph/ContextMenu/NodeContextMenu';
+import { clickHandler, ContextMenuOption, getOptions } from 'pages/Graph/ContextMenu/NodeContextMenu';
 import * as React from 'react';
 import { StyleEdge } from '../styles/styleEdge';
 import { StyleGroup } from '../styles/styleGroup';
@@ -23,7 +23,6 @@ import { store } from 'store/ConfigStore';
 import { NamespaceActions } from 'actions/NamespaceAction';
 import { GraphUrlParams, makeNodeGraphUrlFromParams } from 'components/Nav/NavUtils';
 import { router } from '../../../app/History';
-import { GraphNodeDoubleTapEvent } from 'components/CytoscapeGraph/CytoscapeGraph';
 import { isMultiCluster, serverConfig } from '../../../config';
 import { isParentKiosk, kioskContextMenuAction } from 'components/Kiosk/KioskActions';
 import * as API from '../../../services/Api';
@@ -42,8 +41,8 @@ type ContextMenuOptionPF = ContextMenuOption & {
   node?: GraphElement;
 };
 
-const doubleTapHandler = (node: GraphElement, kiosk: string): void => {
-  handleDoubleTap(node, kiosk);
+const graphNavHandler = (node: GraphElement, kiosk: string): void => {
+  handleGraphNav(node, kiosk);
 };
 
 const promises = new PromisesRegistry();
@@ -64,7 +63,7 @@ const nodeContextMenu = (node: GraphElement, kiosk: string): Promise<React.React
       nodeData.isOutside
         ? ({
             text: 'Namespace Graph',
-            altClickHandler: doubleTapHandler,
+            altClickHandler: graphNavHandler,
             external: false,
             node: node,
             target: '',
@@ -72,7 +71,7 @@ const nodeContextMenu = (node: GraphElement, kiosk: string): Promise<React.React
           } as ContextMenuOptionPF)
         : ({
             text: 'Node Graph',
-            altClickHandler: doubleTapHandler,
+            altClickHandler: graphNavHandler,
             external: false,
             node: node,
             target: '',
@@ -188,21 +187,19 @@ const handleDeleteTrafficRouting = (key: string, node: GraphElement, serviceDeta
   node.getGraph().getData().onDeleteTrafficRouting(key, serviceDetails);
 };
 
-// This is temporary until the PFT graph properly handles DoubleTap.  Until then
-// we offer a ContextMenu option for what would normally be handled via DoubleTap.
-const handleDoubleTap = (doubleTapNode: GraphElement, kiosk: string): void => {
-  const dtNodeData = doubleTapNode.getData() as DecoratedGraphNodeData;
-  const graphData = doubleTapNode.getGraph().getData().graphData;
+const handleGraphNav = (fromNode: GraphElement, kiosk: string): void => {
+  const fromNodeData = fromNode.getData() as DecoratedGraphNodeData;
+  const graphData = fromNode.getGraph().getData().graphData;
 
   if (
-    dtNodeData.isInaccessible ||
-    dtNodeData.isServiceEntry ||
-    (dtNodeData.nodeType === NodeType.BOX && dtNodeData.isBox !== BoxByType.APP)
+    fromNodeData.isInaccessible ||
+    fromNodeData.isServiceEntry ||
+    (fromNodeData.nodeType === NodeType.BOX && fromNodeData.isBox !== BoxByType.APP)
   ) {
     return;
   }
 
-  if (dtNodeData.isOutOfMesh) {
+  if (fromNodeData.isOutOfMesh) {
     if (!serverConfig.ambientEnabled) {
       AlertUtils.add(
         `A node with a missing sidecar provides no node-specific telemetry and can not provide a node detail graph.`,
@@ -219,7 +216,7 @@ const handleDoubleTap = (doubleTapNode: GraphElement, kiosk: string): void => {
 
     return;
   }
-  if (dtNodeData.isIdle) {
+  if (fromNodeData.isIdle) {
     AlertUtils.add(
       `An idle node has no node-specific traffic and can not provide a node detail graph.`,
       undefined,
@@ -227,56 +224,56 @@ const handleDoubleTap = (doubleTapNode: GraphElement, kiosk: string): void => {
     );
     return;
   }
-  if (dtNodeData.isOutside) {
-    store.dispatch(NamespaceActions.setActiveNamespaces([{ name: dtNodeData.namespace }]));
+  if (fromNodeData.isOutside) {
+    store.dispatch(NamespaceActions.setActiveNamespaces([{ name: fromNodeData.namespace }]));
     return;
   }
 
   // If graph is in the drilled-down view, there is the chance that the user
-  // double clicked the same node as in the full graph. Determine if this is
-  // the case.
+  // selected the same node as in the full graph. Determine if this is the case.
+  // note = this may not be a concern in PF graph, but I'm not sure...
   let sameNode = false;
   const node = graphData.fetchParams.node;
   if (node) {
-    sameNode = node && node.nodeType === dtNodeData.nodeType;
-    switch (dtNodeData.nodeType) {
+    sameNode = node && node.nodeType === fromNodeData.nodeType;
+    switch (fromNodeData.nodeType) {
       case NodeType.AGGREGATE:
-        sameNode = sameNode && node.aggregate === dtNodeData.aggregate;
-        sameNode = sameNode && node.aggregateValue === dtNodeData.aggregateValue;
+        sameNode = sameNode && node.aggregate === fromNodeData.aggregate;
+        sameNode = sameNode && node.aggregateValue === fromNodeData.aggregateValue;
         break;
       case NodeType.APP:
-        sameNode = sameNode && node.app === dtNodeData.app;
-        sameNode = sameNode && node.version === dtNodeData.version;
+        sameNode = sameNode && node.app === fromNodeData.app;
+        sameNode = sameNode && node.version === fromNodeData.version;
         break;
       case NodeType.BOX:
         // we only support node graphs on app boxes, so assume app box
-        sameNode = sameNode && node.app === dtNodeData.app;
+        sameNode = sameNode && node.app === fromNodeData.app;
         break;
       case NodeType.SERVICE:
-        sameNode = sameNode && node.service === dtNodeData.service;
+        sameNode = sameNode && node.service === fromNodeData.service;
         break;
       case NodeType.WORKLOAD:
-        sameNode = sameNode && node.workload === dtNodeData.workload;
+        sameNode = sameNode && node.workload === fromNodeData.workload;
         break;
       default:
         sameNode = true; // don't navigate to unsupported node type
     }
   }
 
-  const { app, cluster, namespace, nodeType, service, version, workload } = dtNodeData;
-  const event = { app, cluster, namespace, nodeType, service, version, workload } as GraphNodeDoubleTapEvent;
-  const targetNode: NodeParamsType = { ...event, namespace: { name: dtNodeData.namespace } };
+  const { app, cluster, namespace, nodeType, service, version, workload } = fromNodeData;
+  const event = { app, cluster, namespace, nodeType, service, version, workload } as any;
+  const targetNode: NodeParamsType = { ...event, namespace: { name: fromNodeData.namespace } };
 
-  // If, while in the drilled-down graph, the user double clicked the same
+  // If, while in the drilled-down graph, the user selected the same
   // node as in the main graph, it doesn't make sense to re-load the same view.
   // Instead, assume that the user wants more details for the node and do a
   // redirect to the details page.
   if (sameNode) {
-    handleDoubleTapSameNode(targetNode, kiosk);
+    handleSameNode(targetNode, kiosk);
     return;
   }
 
-  // In case user didn't double-tap the same node, or if graph is in
+  // In case user didn't select the same node, or if graph is in
   // full graph mode, redirect to the drilled-down graph of the chosen node.
   const state = store.getState();
   const urlParams: GraphUrlParams = {
@@ -298,7 +295,7 @@ const handleDoubleTap = (doubleTapNode: GraphElement, kiosk: string): void => {
   };
 
   // To ensure updated components get the updated URL, update the URL first and then the state
-  const nodeGraphUrl = makeNodeGraphUrlFromParams(urlParams, true);
+  const nodeGraphUrl = makeNodeGraphUrlFromParams(urlParams);
 
   if (isParentKiosk(kiosk)) {
     kioskContextMenuAction(nodeGraphUrl);
@@ -308,7 +305,7 @@ const handleDoubleTap = (doubleTapNode: GraphElement, kiosk: string): void => {
 };
 
 // This allows us to navigate to the service details page when zoomed in on nodes
-const handleDoubleTapSameNode = (targetNode: NodeParamsType, kiosk: string): void => {
+const handleSameNode = (targetNode: NodeParamsType, kiosk: string): void => {
   const makeAppDetailsPageUrl = (namespace: string, nodeType: string, name?: string): string => {
     return `/namespaces/${namespace}/${nodeType}/${name}`;
   };
diff --git a/frontend/src/pages/GraphPF/elements/diamond.tsx b/frontend/src/pages/Graph/elements/diamond.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/elements/diamond.tsx
rename to frontend/src/pages/Graph/elements/diamond.tsx
diff --git a/frontend/src/pages/GraphPF/elements/elementFactory.ts b/frontend/src/pages/Graph/elements/elementFactory.ts
similarity index 100%
rename from frontend/src/pages/GraphPF/elements/elementFactory.ts
rename to frontend/src/pages/Graph/elements/elementFactory.ts
diff --git a/frontend/src/pages/GraphPF/elements/extendedBaseEdge.ts b/frontend/src/pages/Graph/elements/extendedBaseEdge.ts
similarity index 100%
rename from frontend/src/pages/GraphPF/elements/extendedBaseEdge.ts
rename to frontend/src/pages/Graph/elements/extendedBaseEdge.ts
diff --git a/frontend/src/pages/GraphPF/elements/plate.tsx b/frontend/src/pages/Graph/elements/plate.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/elements/plate.tsx
rename to frontend/src/pages/Graph/elements/plate.tsx
diff --git a/frontend/src/pages/GraphPF/elements/triangle.tsx b/frontend/src/pages/Graph/elements/triangle.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/elements/triangle.tsx
rename to frontend/src/pages/Graph/elements/triangle.tsx
diff --git a/frontend/src/pages/GraphPF/layouts/ExtendedDagreLayout.ts b/frontend/src/pages/Graph/layouts/ExtendedDagreLayout.ts
similarity index 100%
rename from frontend/src/pages/GraphPF/layouts/ExtendedDagreLayout.ts
rename to frontend/src/pages/Graph/layouts/ExtendedDagreLayout.ts
diff --git a/frontend/src/pages/GraphPF/layouts/layoutFactory.ts b/frontend/src/pages/Graph/layouts/layoutFactory.ts
similarity index 88%
rename from frontend/src/pages/GraphPF/layouts/layoutFactory.ts
rename to frontend/src/pages/Graph/layouts/layoutFactory.ts
index 4804d0b18..2ddf42c29 100644
--- a/frontend/src/pages/GraphPF/layouts/layoutFactory.ts
+++ b/frontend/src/pages/Graph/layouts/layoutFactory.ts
@@ -6,7 +6,7 @@ import {
   LayoutFactory,
   BreadthFirstLayout
 } from '@patternfly/react-topology';
-import { LayoutName } from '../GraphPF';
+import { GraphLayout } from '../GraphPF';
 import { ExtendedDagreLayout } from './ExtendedDagreLayout';
 
 /*
@@ -26,11 +26,11 @@ export const LAYOUT_DEFAULTS: LayoutOptions = {
 
 export const layoutFactory: LayoutFactory = (type: string, graph: Graph): Layout | undefined => {
   switch (type) {
-    case LayoutName.BreadthFirst:
+    case GraphLayout.BreadthFirst:
       return new BreadthFirstLayout(graph);
-    case LayoutName.Concentric:
+    case GraphLayout.Concentric:
       return new ConcentricLayout(graph);
-    case LayoutName.Grid:
+    case GraphLayout.Grid:
       return new GridLayout(graph, {
         nodeDistance: 50
       });
diff --git a/frontend/src/pages/GraphPF/styles/styleEdge.tsx b/frontend/src/pages/Graph/styles/styleEdge.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/styles/styleEdge.tsx
rename to frontend/src/pages/Graph/styles/styleEdge.tsx
diff --git a/frontend/src/pages/GraphPF/styles/styleGroup.tsx b/frontend/src/pages/Graph/styles/styleGroup.tsx
similarity index 100%
rename from frontend/src/pages/GraphPF/styles/styleGroup.tsx
rename to frontend/src/pages/Graph/styles/styleGroup.tsx
diff --git a/frontend/src/pages/GraphPF/styles/styleNode.tsx b/frontend/src/pages/Graph/styles/styleNode.tsx
similarity index 97%
rename from frontend/src/pages/GraphPF/styles/styleNode.tsx
rename to frontend/src/pages/Graph/styles/styleNode.tsx
index 97dcbf928..a6dcc0ffa 100644
--- a/frontend/src/pages/GraphPF/styles/styleNode.tsx
+++ b/frontend/src/pages/Graph/styles/styleNode.tsx
@@ -14,8 +14,8 @@ import * as React from 'react';
 import { KeyIcon, TopologyIcon } from '@patternfly/react-icons';
 import { PFColors } from 'components/Pf/PfColors';
 import { kialiStyle } from 'styles/StyleUtils';
-import { Triangle } from '../elements/triangle';
-import { Plate } from '../elements/plate';
+import { Triangle } from '../../Graph/elements/triangle';
+import { Plate } from '../../Graph/elements/plate';
 
 // This is the registered Node component override that utilizes our customized Node.tsx component.
 
diff --git a/frontend/src/pages/Mesh/Mesh.tsx b/frontend/src/pages/Mesh/Mesh.tsx
index 35c6c2347..3b9a041ba 100644
--- a/frontend/src/pages/Mesh/Mesh.tsx
+++ b/frontend/src/pages/Mesh/Mesh.tsx
@@ -28,9 +28,8 @@ import {
   GraphAreaSelectedEventListener,
   GRAPH_AREA_SELECTED_EVENT
 } from '@patternfly/react-topology';
-import { Layout } from 'types/Graph';
 import { elementFactory } from './elements/elementFactory';
-import { layoutFactory } from './layouts/layoutFactory';
+import { getValidMeshLayout, layoutFactory, MeshLayoutType, MeshLayout } from './layouts/layoutFactory';
 import { TimeInMilliseconds } from 'types/Common';
 import { HistoryManager, URLParam } from 'app/History';
 import { TourStop } from 'components/Tour/TourStop';
@@ -50,9 +49,6 @@ import {
 } from './MeshElems';
 import { elems, setObserved } from 'helpers/GraphHelpers';
 import { MeshTourStops } from './MeshHelpTour';
-import { KialiMeshDagre } from './layouts/KialiMeshDagre';
-//import { KialiMeshCola } from './layouts/KialiMeshCola';
-import { KialiDagreGraph } from 'components/CytoscapeGraph/graphs/KialiDagreGraph';
 import { KialiIcon } from 'config/KialiIcon';
 import { toolbarActiveStyle } from 'styles/GraphStyle';
 
@@ -63,33 +59,10 @@ const ZOOM_OUT = 3 / 4;
 
 export const FIT_PADDING = 90;
 
-export enum LayoutName {
-  Dagre = 'dagre',
-  //MeshCola = 'kiali-mesh-cola',
-  MeshDagre = 'kiali-mesh-dagre'
-}
-
-export enum LayoutType {
-  Layout = 'layout',
-  LayoutNoFit = 'layoutNoFit',
-  Resize = 'resize'
-}
-
 let initialLayout = false;
-let layoutInProgress: LayoutType | undefined;
-
-export function getLayoutByName(layoutName: string): Layout {
-  switch (layoutName) {
-    // case LayoutName.MeshCola:
-    // return KialiMeshCola.getLayout();
-    case LayoutName.MeshDagre:
-      return KialiMeshDagre.getLayout();
-    default:
-      return KialiDagreGraph.getLayout();
-  }
-}
+let layoutInProgress: MeshLayoutType | undefined;
 
-export function meshLayout(controller: Controller, layoutType: LayoutType, reset: boolean = true): void {
+export function layoutMesh(controller: Controller, layoutType: MeshLayoutType, reset: boolean = true): void {
   if (!controller?.hasGraph()) {
     console.debug('Skip meshLayout, no graph');
     return;
@@ -115,12 +88,12 @@ const TopologyContent: React.FC<{
   controller: Controller;
   highlighter: MeshHighlighter;
   isMiniMesh: boolean;
-  layoutName: LayoutName;
+  layout: MeshLayout;
   meshData: MeshData;
   onEdgeTap?: (edge: Edge<EdgeModel>) => void;
   onNodeTap?: (node: Node<NodeModel>) => void;
   onReady: (refs: MeshRefs) => void;
-  setLayout: (val: LayoutName) => void;
+  setLayout: (val: MeshLayout) => void;
   setTarget: (meshTarget: MeshTarget) => void;
   setUpdateTime: (val: TimeInMilliseconds) => void;
   showLegend: boolean;
@@ -129,7 +102,7 @@ const TopologyContent: React.FC<{
   controller,
   highlighter,
   isMiniMesh,
-  layoutName,
+  layout,
   meshData,
   onEdgeTap,
   onNodeTap,
@@ -199,7 +172,7 @@ const TopologyContent: React.FC<{
   // Layout and resize handling
   //
   const handleResize = React.useCallback(() => {
-    meshLayout(controller, LayoutType.Resize);
+    layoutMesh(controller, MeshLayoutType.Resize);
   }, [controller]);
 
   const onLayoutEnd = React.useCallback(() => {
@@ -210,12 +183,12 @@ const TopologyContent: React.FC<{
       return;
     }
 
-    if (layoutInProgress !== LayoutType.LayoutNoFit) {
+    if (layoutInProgress !== MeshLayoutType.LayoutNoFit) {
       controller.getGraph().fit(FIT_PADDING);
 
       // On a resize, perform a delayed second fit, this one is performed [hopefully] after
       // the canvas size is updated (which needs to happen in the underlying PFT code)
-      if (layoutInProgress === LayoutType.Resize) {
+      if (layoutInProgress === MeshLayoutType.Resize) {
         setTimeout(() => {
           controller.getGraph().fit(FIT_PADDING);
         }, 500);
@@ -257,7 +230,7 @@ const TopologyContent: React.FC<{
           graph: {
             id: 'mesh',
             type: 'graph',
-            layout: layoutName
+            layout: layout
           }
         };
         controller.fromModel(defaultModel, false);
@@ -420,7 +393,7 @@ const TopologyContent: React.FC<{
 
     // notify that the graph has been updated
     setUpdateTime(Date.now());
-  }, [controller, meshData, highlighter, layoutName, onReady, setDetailsLevel, setSelectedIds, setUpdateTime]);
+  }, [controller, meshData, highlighter, layout, onReady, setDetailsLevel, setSelectedIds, setUpdateTime]);
 
   //TODO REMOVE THESE DEBUGGING MESSAGES...
   // Leave them for now, they are just good for understanding state changes while we develop this PFT graph.
@@ -432,7 +405,7 @@ const TopologyContent: React.FC<{
   React.useEffect(() => {
     console.debug(`meshData changed, elementsChange=${meshData.elementsChanged}`);
     if (meshData.elementsChanged) {
-      meshLayout(controller, LayoutType.Layout);
+      layoutMesh(controller, MeshLayoutType.Layout);
     }
   }, [controller, meshData]);
 
@@ -464,9 +437,9 @@ const TopologyContent: React.FC<{
       return;
     }
 
-    controller.getGraph().setLayout(layoutName);
-    meshLayout(controller, LayoutType.Layout);
-  }, [controller, layoutName]);
+    controller.getGraph().setLayout(layout);
+    layoutMesh(controller, MeshLayoutType.Layout);
+  }, [controller, layout]);
 
   //
   // Set back to mesh target at unmount-time (not every post-render)
@@ -523,13 +496,11 @@ const TopologyContent: React.FC<{
                       ariaLabel: 'Layout - Dagre',
                       id: 'toolbar_layout_dagre',
                       icon: (
-                        <KialiIcon.Topology
-                          className={LayoutName.Dagre === layoutName ? toolbarActiveStyle : undefined}
-                        />
+                        <KialiIcon.Topology className={MeshLayout.Dagre === layout ? toolbarActiveStyle : undefined} />
                       ),
                       tooltip: 'Layout - Dagre',
                       callback: () => {
-                        _setLayoutName(LayoutName.Dagre);
+                        _setLayoutName(MeshLayout.Dagre);
                       }
                     },
                     {
@@ -537,12 +508,12 @@ const TopologyContent: React.FC<{
                       id: 'toolbar_layout_mesh_dagre',
                       icon: (
                         <KialiIcon.Topology
-                          className={LayoutName.MeshDagre === layoutName ? toolbarActiveStyle : undefined}
+                          className={MeshLayout.MeshDagre === layout ? toolbarActiveStyle : undefined}
                         />
                       ),
                       tooltip: 'Layout - Mesh Dagre',
                       callback: () => {
-                        _setLayoutName(LayoutName.MeshDagre);
+                        _setLayoutName(MeshLayout.MeshDagre);
                       }
                     }
                   ],
@@ -559,7 +530,7 @@ const TopologyContent: React.FC<{
                     controller.getGraph().fit(FIT_PADDING);
                   },
                   resetViewCallback: () => {
-                    meshLayout(controller, LayoutType.Layout);
+                    layoutMesh(controller, MeshLayoutType.Layout);
                   },
                   legend: true,
                   legendIcon: <KialiIcon.Map className={showLegend ? toolbarActiveStyle : undefined} />,
@@ -581,12 +552,12 @@ const TopologyContent: React.FC<{
 
 export const Mesh: React.FC<{
   isMiniMesh: boolean;
-  layout: Layout;
+  layout: MeshLayout;
   meshData: MeshData;
   onEdgeTap?: (edge: Edge<EdgeModel>) => void;
   onNodeTap?: (node: Node<NodeModel>) => void;
   onReady: (refs: MeshRefs) => void;
-  setLayout: (layout: Layout) => void;
+  setLayout: (layout: MeshLayout) => void;
   setTarget: (meshTarget: MeshTarget) => void;
   setUpdateTime: (val: TimeInMilliseconds) => void;
   showLegend: boolean;
@@ -619,20 +590,10 @@ export const Mesh: React.FC<{
     setHighlighter(new MeshHighlighter(c));
   }, []);
 
-  const getLayoutName = (layout: Layout): LayoutName => {
-    switch (layout.name) {
-      // case LayoutName.MeshCola:
-      case LayoutName.MeshDagre:
-        return layout.name;
-      default:
-        return LayoutName.Dagre;
-    }
-  };
-
-  const setLayoutByName = (layoutName: LayoutName): void => {
-    const layout = getLayoutByName(layoutName);
-    HistoryManager.setParam(URLParam.MESH_LAYOUT, layout.name);
-    setLayout(layout);
+  const setLayoutByName = (layout: MeshLayout): void => {
+    const validLayout = getValidMeshLayout(layout);
+    HistoryManager.setParam(URLParam.MESH_LAYOUT, validLayout);
+    setLayout(validLayout);
   };
 
   if (!controller || !meshData || meshData.isLoading) {
@@ -651,7 +612,7 @@ export const Mesh: React.FC<{
         meshData={meshData}
         highlighter={highlighter!}
         isMiniMesh={isMiniMesh}
-        layoutName={getLayoutName(layout)}
+        layout={layout}
         onEdgeTap={onEdgeTap}
         onNodeTap={onNodeTap}
         onReady={onReady}
diff --git a/frontend/src/pages/Mesh/MeshElems.tsx b/frontend/src/pages/Mesh/MeshElems.tsx
index fd8b0bc84..3d33ca29e 100644
--- a/frontend/src/pages/Mesh/MeshElems.tsx
+++ b/frontend/src/pages/Mesh/MeshElems.tsx
@@ -18,7 +18,6 @@ import { NamespaceInfo } from 'types/NamespaceInfo';
 import { PFColors } from 'components/Pf/PfColors';
 
 // Utilities for working with PF Topology
-// - most of these add cytoscape-like functions
 
 export type NodeMap = Map<string, NodeModel>;
 
diff --git a/frontend/src/pages/Mesh/MeshHighlighter.ts b/frontend/src/pages/Mesh/MeshHighlighter.ts
index 9c8e0f57e..47c18c8d8 100644
--- a/frontend/src/pages/Mesh/MeshHighlighter.ts
+++ b/frontend/src/pages/Mesh/MeshHighlighter.ts
@@ -1,6 +1,6 @@
 // There are three special states for a Node or Edge:
 //
-//   - selected: The node has been single-clicked and is 'selected' in cytoscape
+//   - selected: The node has been single-clicked and is 'selected' on the graph.
 //   - hovered: The mouse is currently over the element.  It is marked with HoveredClass
 //   - highlighted: The element is to be emphasized (see below).  It is marked with HighlightClass.
 //   - unhighlighted: The element is to be de-emphasized (see below).  It is marked with UnhighlightClass.
diff --git a/frontend/src/pages/Mesh/MeshPage.tsx b/frontend/src/pages/Mesh/MeshPage.tsx
index 1f1f747dc..de080bee8 100644
--- a/frontend/src/pages/Mesh/MeshPage.tsx
+++ b/frontend/src/pages/Mesh/MeshPage.tsx
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
 import FlexView from 'react-flexview';
 import { kialiStyle } from 'styles/StyleUtils';
 import { DurationInSeconds, IntervalInMilliseconds, TimeInMilliseconds, TimeInSeconds } from '../../types/Common';
-import { Layout, UNKNOWN } from '../../types/Graph';
+import { UNKNOWN } from '../../types/Graph';
 import * as AlertUtils from '../../utils/AlertUtils';
 import { ErrorBoundary } from '../../components/ErrorBoundary/ErrorBoundary';
 import {
@@ -32,7 +32,7 @@ import {
   MeshDefinition,
   MeshTarget
 } from 'types/Mesh';
-import { Mesh, getLayoutByName } from './Mesh';
+import { Mesh } from './Mesh';
 import { MeshActions } from 'actions/MeshActions';
 import { MeshLegend } from './MeshLegend';
 import { MeshToolbarActions } from 'actions/MeshToolbarActions';
@@ -42,6 +42,7 @@ import { MeshTour } from './MeshHelpTour';
 import { MeshThunkActions } from 'actions/MeshThunkActions';
 import { toRangeString } from 'components/Time/Utils';
 import { HistoryManager, URLParam } from 'app/History';
+import { getValidMeshLayout, MeshLayout } from './layouts/layoutFactory';
 
 type ReduxStateProps = {
   activeTour?: TourInfo;
@@ -51,7 +52,7 @@ type ReduxStateProps = {
   isPageVisible: boolean;
   istioAPIEnabled: boolean;
   kiosk: string;
-  layout: Layout;
+  layout: MeshLayout;
   mtlsEnabled: boolean;
   refreshInterval: IntervalInMilliseconds;
   showGateways: boolean;
@@ -65,7 +66,7 @@ type ReduxDispatchProps = {
   endTour: () => void;
   onReady: (controller: Controller) => void;
   setDefinition: (meshDefinition: MeshDefinition) => void;
-  setLayout: (layout: Layout) => void;
+  setLayout: (layout: MeshLayout) => void;
   setTarget: (target: MeshTarget) => void;
   setUpdateTime: (val: TimeInMilliseconds) => void;
   startTour: ({ info, stop }) => void;
@@ -162,11 +163,13 @@ class MeshPageComponent extends React.Component<MeshPageProps, MeshPageState> {
     const urlLayout = HistoryManager.getParam(URLParam.MESH_LAYOUT);
 
     if (urlLayout) {
-      if (urlLayout !== this.props.layout.name) {
-        this.props.setLayout(getLayoutByName(urlLayout));
+      const validLayout = getValidMeshLayout(urlLayout);
+      if (validLayout !== this.props.layout) {
+        this.props.setLayout(validLayout);
+        HistoryManager.setParam(URLParam.MESH_LAYOUT, validLayout);
       }
     } else {
-      HistoryManager.setParam(URLParam.MESH_LAYOUT, this.props.layout.name);
+      HistoryManager.setParam(URLParam.MESH_LAYOUT, this.props.layout);
     }
 
     // Connect to mesh data source updates
@@ -194,7 +197,7 @@ class MeshPageComponent extends React.Component<MeshPageProps, MeshPageState> {
       this.loadMeshFromBackend();
     }
 
-    if (prev.layout.name !== curr.layout.name) {
+    if (prev.layout !== curr.layout) {
       this.errorBoundaryRef.current.cleanError();
     }
 
diff --git a/frontend/src/pages/Mesh/layouts/KialiMeshCola.ts b/frontend/src/pages/Mesh/layouts/KialiMeshCola.ts
deleted file mode 100644
index ab8b812a7..000000000
--- a/frontend/src/pages/Mesh/layouts/KialiMeshCola.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// Used only for Mesh page (PFT graphs only care about the name)
-// TODO: Fix up the graph definition handling when Cytoscape is retired
-export class KialiMeshCola {
-  static getLayout() {
-    return {
-      name: 'kiali-mesh-cola'
-    };
-  }
-}
diff --git a/frontend/src/pages/Mesh/layouts/KialiMeshDagre.ts b/frontend/src/pages/Mesh/layouts/KialiMeshDagre.ts
deleted file mode 100644
index bf7499707..000000000
--- a/frontend/src/pages/Mesh/layouts/KialiMeshDagre.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// Used only for Mesh page (PFT graphs only care about the name)
-// TODO: Fix up the graph definition handling when Cytoscape is retired
-export class KialiMeshDagre {
-  static getLayout() {
-    return {
-      name: 'kiali-mesh-dagre'
-    };
-  }
-}
diff --git a/frontend/src/pages/Mesh/layouts/MeshDagreLayout.ts b/frontend/src/pages/Mesh/layouts/MeshDagreLayout.ts
index 426c312eb..b9e4b9327 100644
--- a/frontend/src/pages/Mesh/layouts/MeshDagreLayout.ts
+++ b/frontend/src/pages/Mesh/layouts/MeshDagreLayout.ts
@@ -29,7 +29,7 @@ export interface ChildGroup {
   groups: LayoutGroup[];
 }
 
-// MeshLayout is a copy of PFT's DagreGroupsLayout, but for innermost groups it copies the simple
+// MeshDagreLayout is a copy of PFT's DagreGroupsLayout, but for innermost groups it copies the simple
 // logic of GridLayout, which works better when organizing nodes with few, or no edges.
 export class MeshDagreLayout extends BaseLayout implements Layout {
   protected dagreOptions: DagreLayoutOptions;
diff --git a/frontend/src/pages/Mesh/layouts/layoutFactory.ts b/frontend/src/pages/Mesh/layouts/layoutFactory.ts
index 12d4c53fe..67e46142f 100644
--- a/frontend/src/pages/Mesh/layouts/layoutFactory.ts
+++ b/frontend/src/pages/Mesh/layouts/layoutFactory.ts
@@ -1,6 +1,5 @@
 import { DagreLayout, Graph, Layout, LayoutFactory } from '@patternfly/react-topology';
 import { MeshDagreLayout } from './MeshDagreLayout';
-import { LayoutName } from '../Mesh';
 //import { MeshColaLayout } from './MeshColaLayout';
 
 /*
@@ -18,13 +17,35 @@ export const LAYOUT_DEFAULTS: LayoutOptions = {
 };
 */
 
+export enum MeshLayoutType {
+  Layout = 'layout',
+  LayoutNoFit = 'layoutNoFit',
+  Resize = 'resize'
+}
+
+// the layouts offered by the mesh page
+export enum MeshLayout {
+  Dagre = 'dagre',
+  //MeshCola = 'kiali-mesh-cola',
+  MeshDagre = 'kiali-mesh-dagre'
+}
+
+export function getValidMeshLayout(layout: string): MeshLayout {
+  switch (layout) {
+    case MeshLayout.MeshDagre:
+      return MeshLayout.MeshDagre;
+    default:
+      return MeshLayout.Dagre;
+  }
+}
+
 export const layoutFactory: LayoutFactory = (type: string, graph: Graph): Layout | undefined => {
   switch (type) {
     //case LayoutName.MeshCola:
     //  return new MeshColaLayout(graph, {
     //    layoutOnDrag: false
     //  });
-    case LayoutName.MeshDagre:
+    case MeshLayout.MeshDagre:
       return new MeshDagreLayout(graph, {
         layoutOnDrag: false,
         ranksep: 15
diff --git a/frontend/src/pages/Mesh/target/TargetPanel.tsx b/frontend/src/pages/Mesh/target/TargetPanel.tsx
index f665a3934..1c5153259 100644
--- a/frontend/src/pages/Mesh/target/TargetPanel.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanel.tsx
@@ -27,7 +27,7 @@ import { meshWideMTLSStatusSelector, minTLSVersionSelector } from 'store/Selecto
 import { TargetPanelDataPlane } from './TargetPanelDataPlane';
 import { TargetPanelControlPlane } from './TargetPanelControlPlane';
 import { useKialiTranslation } from 'utils/I18nUtils';
-import { TargetPanelZtunnel } from './TargetPanelZtunnel';
+import { TargetPanelMetrics } from './TargetPanelMetrics';
 
 type ReduxProps = {
   kiosk: string;
@@ -153,8 +153,9 @@ export const TargetPanelComponent: React.FC<TargetPanelProps> = (props: TargetPa
               />
             );
           case MeshInfraType.ZTUNNEL:
+          case MeshInfraType.KIALI:
             return (
-              <TargetPanelZtunnel
+              <TargetPanelMetrics
                 duration={props.duration}
                 istioAPIEnabled={props.istioAPIEnabled}
                 kiosk={props.kiosk}
diff --git a/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx b/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx
index 576d712ab..29c5f5a98 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx
@@ -233,6 +233,7 @@ export const renderNodeLink = (meshData: MeshNodeData, style?: string): React.Re
       break;
     case MeshInfraType.WAYPOINT:
     case MeshInfraType.ZTUNNEL:
+    case MeshInfraType.KIALI:
       link = `/namespaces/${encodeURIComponent(meshData.namespace)}/workloads/${encodeURIComponent(
         meshData.infraName
       )}`;
diff --git a/frontend/src/pages/Mesh/target/TargetPanelZtunnel.tsx b/frontend/src/pages/Mesh/target/TargetPanelMetrics.tsx
similarity index 73%
rename from frontend/src/pages/Mesh/target/TargetPanelZtunnel.tsx
rename to frontend/src/pages/Mesh/target/TargetPanelMetrics.tsx
index 945ac21db..b38444cc9 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelZtunnel.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelMetrics.tsx
@@ -12,7 +12,7 @@ import { panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle';
 import { useKialiTranslation } from 'utils/I18nUtils';
 import { UNKNOWN } from 'types/Graph';
 import { TargetPanelEditor } from './TargetPanelEditor';
-import { ZtunnelMetricsMap } from '../../../types/Metrics';
+import { ResourceUsageMetricsMap } from '../../../types/Metrics';
 import { TargetPanelControlPlaneMetrics } from './TargetPanelControlPlaneMetrics';
 import * as API from '../../../services/Api';
 import * as AlertUtils from '../../../utils/AlertUtils';
@@ -20,17 +20,17 @@ import { computePrometheusRateParams } from '../../../services/Prometheus';
 import { IstioMetricsOptions } from '../../../types/MetricsOptions';
 import { serverConfig } from '../../../config';
 
-type TargetPanelZtunnelProps<T extends MeshNodeData> = TargetPanelCommonProps & {
+type TargetPanelMetricsProps<T extends MeshNodeData> = TargetPanelCommonProps & {
   target: NodeTarget<T>;
 };
 
-export const TargetPanelZtunnel: React.FC<TargetPanelZtunnelProps<MeshNodeData>> = (
-  props: TargetPanelZtunnelProps<MeshNodeData>
+export const TargetPanelMetrics: React.FC<TargetPanelMetricsProps<MeshNodeData>> = (
+  props: TargetPanelMetricsProps<MeshNodeData>
 ) => {
   const { t } = useKialiTranslation();
 
   const node = props.target;
-  const [metrics, setMetrics] = React.useState<ZtunnelMetricsMap>();
+  const [metrics, setMetrics] = React.useState<ResourceUsageMetricsMap>();
 
   const fetchMetrics = async (): Promise<void> => {
     const rateParams = computePrometheusRateParams(props.duration, 10);
@@ -46,16 +46,21 @@ export const TargetPanelZtunnel: React.FC<TargetPanelZtunnelProps<MeshNodeData>>
 
     const data = props.target.elem.getData();
 
-    return API.getZtunnelMetrics(data?.namespace ? data.namespace : '', 'ztunnel', options, data?.cluster)
+    return API.getResourceUsageMetrics(
+      data?.namespace ? data.namespace : '',
+      data?.infraName ? data.infraName : '',
+      options,
+      data?.cluster
+    )
       .then(response => {
-        const controlPlaneMetrics: ZtunnelMetricsMap = {
-          ztunnel_cpu_usage: response.data.ztunnel_cpu_usage,
-          ztunnel_memory_usage: response.data.ztunnel_memory_usage
+        const controlPlaneMetrics: ResourceUsageMetricsMap = {
+          cpu_usage: response.data.cpu_usage,
+          memory_usage: response.data.memory_usage
         };
         setMetrics(controlPlaneMetrics);
       })
       .catch(error => {
-        AlertUtils.addError('Could not fetch ztunnel metrics.', error);
+        AlertUtils.addError('Could not fetch component metrics.', error);
         throw error;
       });
   };
@@ -63,7 +68,7 @@ export const TargetPanelZtunnel: React.FC<TargetPanelZtunnelProps<MeshNodeData>>
   React.useEffect(() => {
     fetchMetrics();
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [props.duration]);
+  }, [props.duration, props.target.elem]);
 
   if (!node) {
     return null;
@@ -79,9 +84,9 @@ export const TargetPanelZtunnel: React.FC<TargetPanelZtunnelProps<MeshNodeData>>
         {targetPanelHR}
         <TargetPanelControlPlaneMetrics
           key={data.namespace}
-          istiodContainerMemory={metrics?.ztunnel_memory_usage}
-          istiodContainerCpu={metrics?.ztunnel_cpu_usage}
-          type="Ztunnel"
+          istiodContainerMemory={metrics?.memory_usage}
+          istiodContainerCpu={metrics?.cpu_usage}
+          type={data.infraName}
         />
         {targetPanelHR}
 
diff --git a/frontend/src/pages/Mesh/toolbar/MeshFind.tsx b/frontend/src/pages/Mesh/toolbar/MeshFind.tsx
index 6e4ee62fd..1c9aff7cb 100644
--- a/frontend/src/pages/Mesh/toolbar/MeshFind.tsx
+++ b/frontend/src/pages/Mesh/toolbar/MeshFind.tsx
@@ -17,7 +17,6 @@ import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
 import { KialiAppState } from '../../../store/Store';
 import { meshFindValueSelector, meshHideValueSelector } from '../../../store/Selectors';
-import * as CytoscapeGraphUtils from '../../../components/CytoscapeGraph/CytoscapeGraphUtils';
 import { KialiIcon } from 'config/KialiIcon';
 import { kialiStyle } from 'styles/StyleUtils';
 import { TourStop } from 'components/Tour/TourStop';
@@ -27,20 +26,29 @@ import { AutoComplete } from 'utils/AutoComplete';
 import { HEALTHY, NA, NOT_READY } from 'types/Health';
 import { location, HistoryManager, URLParam } from '../../../app/History';
 import { isValid } from 'utils/Common';
-import { setObserved, elems, SelectAnd, SelectExp, selectOr, SelectOr, descendents } from 'helpers/GraphHelpers';
+import {
+  setObserved,
+  elems,
+  SelectAnd,
+  SelectExp,
+  selectOr,
+  SelectOr,
+  descendents,
+  toSafeFieldName
+} from 'helpers/GraphHelpers';
 import { isArray } from 'lodash';
 import { MeshAttr, MeshEdgeData, MeshInfraType, MeshNodeData } from 'types/Mesh';
-import { Layout } from 'types/Graph';
 import { MeshToolbarActions } from 'actions/MeshToolbarActions';
 import { MeshFindOptions } from './MeshFindOptions';
 import { MeshHelpFind } from '../MeshHelpFind';
-import { LayoutType, meshLayout } from '../Mesh';
+import { layoutMesh } from '../Mesh';
 import { infoStyle } from 'styles/IconStyle';
+import { MeshLayoutType, MeshLayout } from '../layouts/layoutFactory';
 
 type ReduxStateProps = {
   findValue: string;
   hideValue: string;
-  layout: Layout;
+  layout: MeshLayout;
   showFindHelp: boolean;
 };
 
@@ -569,7 +577,7 @@ export class MeshFindComponent extends React.Component<MeshFindProps, MeshFindSt
 
     // always perform a full layout, because if this function is invoked at all, we know either we're dealing with either
     // a new controller, a different topology, a new hide expression, etc
-    meshLayout(controller, LayoutType.Layout);
+    layoutMesh(controller, MeshLayoutType.Layout);
   };
 
   private handleFind = (controller: Controller): void => {
@@ -816,7 +824,7 @@ export class MeshFindComponent extends React.Component<MeshFindProps, MeshFindSt
         if (field.startsWith('label:')) {
           return {
             target: 'node',
-            selector: { prop: CytoscapeGraphUtils.toSafeCyFieldName(field), op: op, val: val }
+            selector: { prop: toSafeFieldName(field), op: op, val: val }
           };
         }
 
@@ -886,7 +894,7 @@ export class MeshFindComponent extends React.Component<MeshFindProps, MeshFindSt
       default:
         // special node operand
         if (field.startsWith('label:')) {
-          const safeFieldName = CytoscapeGraphUtils.toSafeCyFieldName(field);
+          const safeFieldName = toSafeFieldName(field);
           return { target: 'node', selector: { prop: safeFieldName, op: isNegation ? '<=' : '>', val: 0 } };
         }
 
diff --git a/frontend/src/pages/Mesh/toolbar/__tests__/MeshFind.test.tsx b/frontend/src/pages/Mesh/toolbar/__tests__/MeshFind.test.tsx
index d81920f5f..b92e94e59 100644
--- a/frontend/src/pages/Mesh/toolbar/__tests__/MeshFind.test.tsx
+++ b/frontend/src/pages/Mesh/toolbar/__tests__/MeshFind.test.tsx
@@ -1,9 +1,11 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import { MeshFindComponent } from '../MeshFind';
+import { MeshLayout } from 'pages/Mesh/layouts/layoutFactory';
 
 const testHandler = () => undefined;
 const testSetter = _val => undefined;
+const layout = MeshLayout.Dagre;
 
 // TODO Find out why typescript is unhappy and get rid of all of these ts-ignores
 describe('Parse find value test', () => {
@@ -18,7 +20,7 @@ describe('Parse find value test', () => {
         setFindValue={testSetter}
         setHideValue={testSetter}
         toggleFindHelp={testHandler}
-        layout={{ name: '' }}
+        layout={layout}
       />
     );
 
diff --git a/frontend/src/pages/ServiceDetails/ServiceInfo.tsx b/frontend/src/pages/ServiceDetails/ServiceInfo.tsx
index 5984e0708..b7bca8c5d 100644
--- a/frontend/src/pages/ServiceDetails/ServiceInfo.tsx
+++ b/frontend/src/pages/ServiceDetails/ServiceInfo.tsx
@@ -33,9 +33,6 @@ import { canCreate, canUpdate } from '../../types/Permissions';
 import { KialiAppState } from '../../store/Store';
 import { durationSelector } from '../../store/Selectors';
 import { ServiceNetwork } from './ServiceNetwork';
-import { GraphEdgeTapEvent } from '../../components/CytoscapeGraph/CytoscapeGraph';
-import { location, router, URLParam } from '../../app/History';
-import { MiniGraphCard } from '../../components/CytoscapeGraph/MiniGraphCard';
 import { IstioConfigCard } from '../../components/IstioConfigCard/IstioConfigCard';
 import { ServiceWizard } from '../../components/IstioWizards/ServiceWizard';
 import { ConfirmDeleteTrafficRoutingModal } from '../../components/IstioWizards/ConfirmDeleteTrafficRoutingModal';
@@ -44,7 +41,7 @@ import { deleteServiceTrafficRouting } from '../../services/Api';
 import * as AlertUtils from '../../utils/AlertUtils';
 import { triggerRefresh } from '../../hooks/refresh';
 import { serverConfig } from 'config';
-import { MiniGraphCardPF } from 'pages/GraphPF/MiniGraphCardPF';
+import { MiniGraphCardPF } from 'pages/Graph/MiniGraphCardPF';
 
 type ReduxProps = {
   duration: DurationInSeconds;
@@ -100,19 +97,6 @@ class ServiceInfoComponent extends React.Component<Props, ServiceInfoState> {
     }
   }
 
-  goToMetrics = (e: GraphEdgeTapEvent): void => {
-    if (e.source !== e.target) {
-      const direction = e.source === this.props.service ? 'outbound' : 'inbound';
-      const destination = direction === 'inbound' ? 'source_canonical_service' : 'destination_canonical_service';
-      const urlParams = new URLSearchParams(location.getSearch());
-
-      urlParams.set('tab', 'metrics');
-      urlParams.set(URLParam.BY_LABELS, `${destination}=${e.source === this.props.service ? e.target : e.source}`);
-
-      router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true });
-    }
-  };
-
   private fetchBackend = (): void => {
     if (!this.props.serviceDetails) {
       return;
@@ -253,11 +237,7 @@ class ServiceInfoComponent extends React.Component<Props, ServiceInfoState> {
     // RenderComponentScroll handles height to provide an inner scroll combined with tabs
     // This height needs to be propagated to minigraph to proper resize in height
     // Graph resizes correctly on width
-    const height = this.state.tabHeight ? this.state.tabHeight - 115 : 300;
-    const graphContainerStyle = kialiStyle({ width: '100%', height: height });
-    const includeMiniGraphCy = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'pf';
-    const includeMiniGraphPF = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'cy';
-    const miniGraphSpan = includeMiniGraphCy && includeMiniGraphPF ? 4 : 8;
+    const miniGraphSpan = 8;
 
     return (
       <>
@@ -283,30 +263,14 @@ class ServiceInfoComponent extends React.Component<Props, ServiceInfoState> {
               </Stack>
             </GridItem>
 
-            {includeMiniGraphCy && (
-              <GridItem span={miniGraphSpan}>
-                <MiniGraphCard
-                  dataSource={this.graphDataSource}
-                  onEdgeTap={this.goToMetrics}
-                  namespace={this.props.namespace}
-                  graphContainerStyle={graphContainerStyle}
-                  serviceDetails={this.props.serviceDetails}
-                  onDeleteTrafficRouting={this.handleDeleteTrafficRouting}
-                  onLaunchWizard={this.handleLaunchWizard}
-                />
-              </GridItem>
-            )}
-
-            {includeMiniGraphPF && (
-              <GridItem span={miniGraphSpan}>
-                <MiniGraphCardPF
-                  dataSource={this.graphDataSource}
-                  onDeleteTrafficRouting={this.handleDeleteTrafficRouting}
-                  onLaunchWizard={this.handleLaunchWizard}
-                  serviceDetails={this.props.serviceDetails}
-                />
-              </GridItem>
-            )}
+            <GridItem span={miniGraphSpan}>
+              <MiniGraphCardPF
+                dataSource={this.graphDataSource}
+                onDeleteTrafficRouting={this.handleDeleteTrafficRouting}
+                onLaunchWizard={this.handleLaunchWizard}
+                serviceDetails={this.props.serviceDetails}
+              />
+            </GridItem>
           </Grid>
         </RenderComponentScroll>
 
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx b/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx
index 834405d87..20cd5232d 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadInfo.tsx
@@ -14,11 +14,8 @@ import { DurationInSeconds } from 'types/Common';
 import { isIstioNamespace, serverConfig } from '../../config/ServerConfig';
 import { gvkType, IstioConfigList, skipUnrelatedK8sGateways, toIstioItems } from '../../types/IstioConfigList';
 import { WorkloadPods } from './WorkloadPods';
-import { GraphEdgeTapEvent } from '../../components/CytoscapeGraph/CytoscapeGraph';
-import { location, router, URLParam } from '../../app/History';
-import { MiniGraphCard } from '../../components/CytoscapeGraph/MiniGraphCard';
 import { IstioConfigCard } from '../../components/IstioConfigCard/IstioConfigCard';
-import { MiniGraphCardPF } from 'pages/GraphPF/MiniGraphCardPF';
+import { MiniGraphCardPF } from 'pages/Graph/MiniGraphCardPF';
 import { getGVKTypeString, stringToGVK } from '../../utils/IstioConfigUtils';
 import { WorkloadEntries } from './WorkloadEntries';
 
@@ -250,22 +247,6 @@ export class WorkloadInfo extends React.Component<WorkloadInfoProps, WorkloadInf
     return validations;
   }
 
-  goToMetrics = (e: GraphEdgeTapEvent): void => {
-    if (e.source !== e.target && this.props.workload) {
-      const direction = e.source === this.props.workload.name ? 'outbound' : 'inbound';
-      const destination = direction === 'inbound' ? 'source_canonical_service' : 'destination_canonical_service';
-
-      const urlParams = new URLSearchParams(location.getSearch());
-      urlParams.set('tab', direction === 'inbound' ? 'in_metrics' : 'out_metrics');
-      urlParams.set(
-        URLParam.BY_LABELS,
-        `${destination}=${e.source === this.props.workload.name ? e.target : e.source}`
-      );
-
-      router.navigate(`${location.getPathname()}?${urlParams.toString()}`, { replace: true });
-    }
-  };
-
   render(): React.ReactNode {
     const workload = this.props.workload;
     const pods = workload?.pods ?? [];
@@ -279,11 +260,7 @@ export class WorkloadInfo extends React.Component<WorkloadInfoProps, WorkloadInf
     // RenderComponentScroll handles height to provide an inner scroll combined with tabs
     // This height needs to be propagated to minigraph to proper resize in height
     // Graph resizes correctly on width
-    const height = this.state.tabHeight ? this.state.tabHeight - 115 : 300;
-    const graphContainerStyle = kialiStyle({ width: '100%', height: height });
-    const includeMiniGraphCy = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'pf';
-    const includeMiniGraphPF = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'cy';
-    const miniGraphSpan = includeMiniGraphCy && includeMiniGraphPF ? 4 : 8;
+    const miniGraphSpan = 8;
 
     return (
       <>
@@ -325,29 +302,14 @@ export class WorkloadInfo extends React.Component<WorkloadInfoProps, WorkloadInf
               </Stack>
             </GridItem>
 
-            {includeMiniGraphCy && (
-              <GridItem span={miniGraphSpan}>
-                <MiniGraphCard
-                  onEdgeTap={this.goToMetrics}
-                  dataSource={this.graphDataSource}
-                  namespace={this.props.namespace}
-                  graphContainerStyle={graphContainerStyle}
-                  workload={this.props.workload}
-                  refreshWorkload={this.props.refreshWorkload}
-                />
-              </GridItem>
-            )}
-
-            {includeMiniGraphPF && (
-              <GridItem span={miniGraphSpan}>
-                <MiniGraphCardPF
-                  dataSource={this.graphDataSource}
-                  namespace={this.props.namespace}
-                  workload={this.props.workload}
-                  refreshWorkload={this.props.refreshWorkload}
-                />
-              </GridItem>
-            )}
+            <GridItem span={miniGraphSpan}>
+              <MiniGraphCardPF
+                dataSource={this.graphDataSource}
+                namespace={this.props.namespace}
+                workload={this.props.workload}
+                refreshWorkload={this.props.refreshWorkload}
+              />
+            </GridItem>
           </Grid>
         </RenderComponentScroll>
       </>
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx b/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
index 8a4c0ffb3..47884af99 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadPodLogs.tsx
@@ -40,7 +40,7 @@ import screenfull, { Screenfull } from 'screenfull';
 import { KialiAppState } from '../../store/Store';
 import { connect } from 'react-redux';
 import { timeRangeSelector } from '../../store/Selectors';
-import { PFColors, PFColorVal } from 'components/Pf/PfColors';
+import { PFColors } from 'components/Pf/PfColors';
 import { AccessLogModal } from 'components/Envoy/AccessLogModal';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { location, router, URLParam } from 'app/History';
@@ -83,7 +83,7 @@ export type WorkloadPodLogsProps = ReduxProps & {
 };
 
 type ContainerOption = {
-  color: PFColorVal;
+  color: PFColors;
   displayName: string;
   isAmbient: boolean;
   isProxy: boolean;
diff --git a/frontend/src/pages/WorkloadDetails/WorkloadPods.tsx b/frontend/src/pages/WorkloadDetails/WorkloadPods.tsx
index b2945607c..615c85a95 100644
--- a/frontend/src/pages/WorkloadDetails/WorkloadPods.tsx
+++ b/frontend/src/pages/WorkloadDetails/WorkloadPods.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { ObjectValidation, Pod } from '../../types/IstioObjects';
-import { IRow, TableVariant, ThProps } from '@patternfly/react-table';
+import { IRow, ISortBy, SortByDirection, TableVariant } from '@patternfly/react-table';
 import {
   Card,
   CardBody,
@@ -19,8 +19,9 @@ import { KialiIcon } from '../../config/KialiIcon';
 import { LocalTime } from '../../components/Time/LocalTime';
 import { Labels } from '../../components/Label/Labels';
 import { PFBadge, PFBadges } from '../../components/Pf/PfBadges';
-import { SimpleTable } from 'components/Table/SimpleTable';
+import { SimpleTable, SortableTh } from 'components/Table/SimpleTable';
 import { infoStyle } from 'styles/IconStyle';
+import { serverConfig } from '../../config';
 
 type WorkloadPodsProps = {
   namespace: string;
@@ -50,7 +51,20 @@ const iconStyle = kialiStyle({
 });
 
 export const WorkloadPods: React.FC<WorkloadPodsProps> = (props: WorkloadPodsProps) => {
-  const columns: ThProps[] = [{ title: 'Name' }, { title: 'Status', width: 10 }];
+  const [sortIndex, setSortIndex] = React.useState<number>(0);
+  const [sortDirection, setSortDirection] = React.useState<SortByDirection>(SortByDirection.asc);
+
+  const columns: SortableTh[] = [
+    { title: 'Name', sortable: true },
+    { title: 'Revision', width: 25, sortable: true },
+    { title: 'Status', width: 20, sortable: true }
+  ];
+
+  const sort: ISortBy = { index: sortIndex, direction: sortDirection };
+  const onSort = (_event: React.MouseEvent, index: number, direction: SortByDirection): void => {
+    setSortIndex(index);
+    setSortDirection(direction);
+  };
 
   const noPods: React.ReactNode = (
     <EmptyState variant={EmptyStateVariant.sm} className={emptyStyle}>
@@ -58,93 +72,101 @@ export const WorkloadPods: React.FC<WorkloadPodsProps> = (props: WorkloadPodsPro
     </EmptyState>
   );
 
-  const rows: IRow[] = props.pods
-    .sort((p1: Pod, p2: Pod) => (p1.name < p2.name ? -1 : 1))
-    .map((pod, _podIdx) => {
-      let validation: ObjectValidation = {} as ObjectValidation;
-
-      if (props.validations[pod.name]) {
-        validation = props.validations[pod.name];
-      }
-
-      const podProperties = (
-        <div key="properties-list" className={resourceListStyle}>
-          <ul style={{ listStyleType: 'none' }}>
-            <li>
-              <span>Created</span>
-              <div style={{ display: 'inline-block' }}>
-                <LocalTime time={pod.createdAt} />
-              </div>
-            </li>
-
-            <li>
-              <span>Created By</span>
-              <div style={{ display: 'inline-block' }}>
-                {pod.createdBy && pod.createdBy.length > 0
-                  ? pod.createdBy.map(ref => `${ref.name} (${ref.kind})`).join(', ')
-                  : 'Not found'}
-              </div>
-            </li>
-
-            <li>
-              <span>Service Account</span>
-              <div style={{ display: 'inline-block' }}>{pod.serviceAccountName ?? 'Not found'}</div>
-            </li>
-
-            <li>
-              <span>Istio Init Container</span>
-              <div style={{ display: 'inline-block' }}>
-                {pod.istioInitContainers ? pod.istioInitContainers.map(c => `${c.image}`).join(', ') : 'Not found'}
-              </div>
-            </li>
-
-            <li>
-              <span>Istio Container</span>
-              <div style={{ display: 'inline-block' }}>
-                {pod.istioContainers ? pod.istioContainers.map(c => `${c.image}`).join(', ') : 'Not found'}
-              </div>
-            </li>
-
-            <li>
-              <span data-test="protocol">Protocol</span>
-              <div style={{ display: 'inline-block' }} data-test="protocol-value">
-                {pod.protocol ? pod.protocol : ''}
-              </div>
-            </li>
-
-            <li>
-              <span>Labels</span>
-              <div style={{ display: 'inline-block' }}>
-                <Labels labels={pod.labels} expanded={true} />
-              </div>
-            </li>
-          </ul>
-        </div>
-      );
-
-      return {
-        cells: [
-          <span>
-            <div key="service-icon" className={iconStyle}>
-              <PFBadge badge={PFBadges.Pod} size="sm" position={TooltipPosition.top} />
+  const sortedPods = [...props.pods].sort((a, b) => {
+    const columnKeys = ['name', 'revision', 'status'];
+    const key = columnKeys[sortIndex];
+
+    const aValue = key === 'revision' ? a.annotations?.[serverConfig.istioLabels.injectionLabelRev] ?? '' : a[key];
+    const bValue = key === 'revision' ? b.annotations?.[serverConfig.istioLabels.injectionLabelRev] ?? '' : b[key];
+
+    if (aValue < bValue) return sortDirection === SortByDirection.asc ? -1 : 1;
+    if (aValue > bValue) return sortDirection === SortByDirection.asc ? 1 : -1;
+    return 0;
+  });
+
+  const rows: IRow[] = sortedPods.map((pod, _podIdx) => {
+    let validation: ObjectValidation = {} as ObjectValidation;
+
+    if (props.validations[pod.name]) {
+      validation = props.validations[pod.name];
+    }
+
+    const podProperties = (
+      <div key="properties-list" className={resourceListStyle}>
+        <ul style={{ listStyleType: 'none' }}>
+          <li>
+            <span>Created</span>
+            <div style={{ display: 'inline-block' }}>
+              <LocalTime time={pod.createdAt} />
             </div>
+          </li>
+
+          <li>
+            <span>Created By</span>
+            <div style={{ display: 'inline-block' }}>
+              {pod.createdBy && pod.createdBy.length > 0
+                ? pod.createdBy.map(ref => `${ref.name} (${ref.kind})`).join(', ')
+                : 'Not found'}
+            </div>
+          </li>
+
+          <li>
+            <span>Service Account</span>
+            <div style={{ display: 'inline-block' }}>{pod.serviceAccountName ?? 'Not found'}</div>
+          </li>
 
-            {pod.name}
+          <li>
+            <span>Istio Init Container</span>
+            <div style={{ display: 'inline-block' }}>
+              {pod.istioInitContainers ? pod.istioInitContainers.map(c => `${c.image}`).join(', ') : 'Not found'}
+            </div>
+          </li>
 
-            <Tooltip
-              position={TooltipPosition.right}
-              content={<div style={{ textAlign: 'left' }}>{podProperties}</div>}
-            >
-              <span data-test="pod-info">
-                <KialiIcon.Info className={infoStyle} />
-              </span>
-            </Tooltip>
-          </span>,
+          <li>
+            <span>Istio Container</span>
+            <div style={{ display: 'inline-block' }}>
+              {pod.istioContainers ? pod.istioContainers.map(c => `${c.image}`).join(', ') : 'Not found'}
+            </div>
+          </li>
 
-          <PodStatus proxyStatus={pod.proxyStatus} checks={validation.checks} />
-        ]
-      };
-    });
+          <li>
+            <span data-test="protocol">Protocol</span>
+            <div style={{ display: 'inline-block' }} data-test="protocol-value">
+              {pod.protocol ? pod.protocol : ''}
+            </div>
+          </li>
+
+          <li>
+            <span>Labels</span>
+            <div style={{ display: 'inline-block' }}>
+              <Labels labels={pod.labels} expanded={true} />
+            </div>
+          </li>
+        </ul>
+      </div>
+    );
+
+    return {
+      cells: [
+        <span>
+          <div key="service-icon" className={iconStyle}>
+            <PFBadge badge={PFBadges.Pod} size="sm" position={TooltipPosition.top} />
+          </div>
+
+          {pod.name}
+
+          <Tooltip position={TooltipPosition.right} content={<div style={{ textAlign: 'left' }}>{podProperties}</div>}>
+            <span data-test="pod-info">
+              <KialiIcon.Info className={infoStyle} />
+            </span>
+          </Tooltip>
+        </span>,
+        <span>{pod.annotations?.[serverConfig.istioLabels.injectionLabelRev] ?? 'N/A'}</span>,
+
+        <PodStatus proxyStatus={pod.proxyStatus} checks={validation.checks} />
+      ]
+    };
+  });
 
   return (
     <Card isCompact={true} id="WorkloadPodsCard">
@@ -161,6 +183,8 @@ export const WorkloadPods: React.FC<WorkloadPodsProps> = (props: WorkloadPodsPro
           rows={rows}
           variant={TableVariant.compact}
           emptyState={noPods}
+          sortBy={sort}
+          onSort={onSort}
         />
       </CardBody>
     </Card>
diff --git a/frontend/src/reducers/GraphDataState.ts b/frontend/src/reducers/GraphDataState.ts
index 65f7c19de..cf742ffa6 100644
--- a/frontend/src/reducers/GraphDataState.ts
+++ b/frontend/src/reducers/GraphDataState.ts
@@ -5,13 +5,13 @@ import { GraphState } from '../store/Store';
 import { EdgeMode, GraphType, TrafficRate } from '../types/Graph';
 import { GraphToolbarActions } from '../actions/GraphToolbarActions';
 import { updateState } from '../utils/Reducer';
-import { KialiDagreGraph } from '../components/CytoscapeGraph/graphs/KialiDagreGraph';
+import { GraphLayout } from 'pages/Graph/GraphPF';
 
 export const INITIAL_GRAPH_STATE: GraphState = {
   edgeMode: EdgeMode.ALL,
   graphDefinition: null,
-  layout: KialiDagreGraph.getLayout(),
-  namespaceLayout: KialiDagreGraph.getLayout(),
+  layout: GraphLayout.Dagre,
+  namespaceLayout: GraphLayout.Dagre,
   node: undefined,
   rankResult: {
     upperBound: 0
@@ -82,7 +82,6 @@ export const GraphDataStateReducer = (state: GraphState = INITIAL_GRAPH_STATE, a
     case getType(GraphActions.updateSummary):
       return updateState(state, {
         summaryData: updateState(state.summaryData, {
-          isPF: action.payload.isPF,
           summaryType: action.payload.summaryType,
           summaryTarget: action.payload.summaryTarget
         })
diff --git a/frontend/src/reducers/MeshDataState.ts b/frontend/src/reducers/MeshDataState.ts
index 354a3e85d..a094e36f8 100644
--- a/frontend/src/reducers/MeshDataState.ts
+++ b/frontend/src/reducers/MeshDataState.ts
@@ -4,11 +4,11 @@ import { MeshState } from '../store/Store';
 import { updateState } from '../utils/Reducer';
 import { MeshActions } from 'actions/MeshActions';
 import { MeshToolbarActions } from 'actions/MeshToolbarActions';
-import { KialiDagreGraph } from 'components/CytoscapeGraph/graphs/KialiDagreGraph';
+import { MeshLayout } from 'pages/Mesh/layouts/layoutFactory';
 
 export const INITIAL_MESH_STATE: MeshState = {
   definition: null,
-  layout: KialiDagreGraph.getLayout(),
+  layout: MeshLayout.Dagre,
   target: null,
   toolbarState: {
     findValue: '',
diff --git a/frontend/src/reducers/__tests__/GraphDataState.test.ts b/frontend/src/reducers/__tests__/GraphDataState.test.ts
index 047e1cba0..2e910bb80 100644
--- a/frontend/src/reducers/__tests__/GraphDataState.test.ts
+++ b/frontend/src/reducers/__tests__/GraphDataState.test.ts
@@ -3,15 +3,16 @@ import { GraphActions } from '../../actions/GraphActions';
 import { GlobalActions } from '../../actions/GlobalActions';
 import { DefaultTrafficRates, EdgeMode, GraphType } from '../../types/Graph';
 import { GraphState } from 'store/Store';
-import { KialiDagreGraph } from '../../components/CytoscapeGraph/graphs/KialiDagreGraph';
+import { GraphElement } from '@patternfly/react-topology';
+import { GraphLayout } from 'pages/Graph/GraphPF';
 
 describe('GraphDataState', () => {
   it('should return the initial state', () => {
     expect(GraphDataStateReducer(undefined, GlobalActions.unknown())).toEqual({
       edgeMode: EdgeMode.ALL,
       graphDefinition: null,
-      layout: KialiDagreGraph.getLayout(),
-      namespaceLayout: KialiDagreGraph.getLayout(),
+      layout: GraphLayout.Dagre,
+      namespaceLayout: GraphLayout.Dagre,
       node: undefined,
       rankResult: { upperBound: 0 },
       summaryData: null,
@@ -42,9 +43,9 @@ describe('GraphDataState', () => {
   });
 
   it('should handle UPDATE_SUMMARY', () => {
-    const action = GraphActions.updateSummary({ summaryType: 'node', summaryTarget: 'mynode' });
+    const action = GraphActions.updateSummary({ summaryType: 'node', summaryTarget: {} as GraphElement });
     const updatedState = GraphDataStateReducer(undefined, action);
 
-    expect(updatedState.summaryData).toEqual({ summaryType: 'node', summaryTarget: 'mynode' });
+    expect(updatedState.summaryData).toEqual({ summaryType: 'node', summaryTarget: {} as GraphElement });
   });
 });
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index e4c41293f..90a626ba1 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -6,16 +6,14 @@ import { ServiceListPage } from './pages/ServiceList/ServiceListPage';
 import { IstioConfigListPage } from './pages/IstioConfigList/IstioConfigListPage';
 import { AppListPage } from './pages/AppList/AppListPage';
 import { OverviewPage } from './pages/Overview/OverviewPage';
-import { GraphPage } from 'pages/Graph/GraphPage';
 import { MeshPage } from 'pages/Mesh/MeshPage';
-import { GraphRoute } from 'routes/GraphRoute';
 import { ServiceDetailsRoute } from 'routes/ServiceDetailsRoute';
 import { WorkloadDetailsRoute } from 'routes/WorkloadDetailsRoute';
 import { AppDetailsRoute } from 'routes/AppDetailsRoute';
 import { IstioConfigDetailsRoute } from 'routes/IstioConfigDetailsRoute';
 import { IstioConfigNewRoute } from 'routes/IstioConfigNewRoute';
 import { GraphRoutePF } from 'routes/GraphRoutePF';
-import { GraphPagePF } from 'pages/GraphPF/GraphPagePF';
+import { GraphPagePF } from 'pages/Graph/GraphPagePF';
 import { t } from 'utils/I18nUtils';
 import { RouteObject } from 'react-router-dom-v5-compat';
 import { WildcardRoute } from 'routes/WildcardRoute';
@@ -32,14 +30,8 @@ const navMenuItems: MenuItem[] = [
     pathsActive: [/^\/overview\/(.*)/]
   },
   {
-    id: 'traffic_graph_cy',
-    title: t('Traffic Graph [Cy]'),
-    to: '/graph/namespaces/',
-    pathsActive: [/^\/graph\/(.*)/]
-  },
-  {
-    id: 'traffic_graph_pf',
-    title: t('Traffic Graph [PF]'),
+    id: 'traffic_graph',
+    title: t('Traffic Graph'),
     to: '/graphpf/namespaces/',
     pathsActive: [/^\/graphpf\/(.*)/]
   },
@@ -84,30 +76,6 @@ const pathRoutes: RouteObject[] = [
     path: '/overview',
     element: <OverviewPage />
   },
-  {
-    path: `/graph/node/namespaces/:namespace/${Paths.AGGREGATES}/:aggregate/:aggregateValue`,
-    element: <GraphRoute />
-  },
-  {
-    path: `/graph/node/namespaces/:namespace/${Paths.APPLICATIONS}/:app/versions/:version`,
-    element: <GraphRoute />
-  },
-  {
-    path: `/graph/node/namespaces/:namespace/${Paths.APPLICATIONS}/:app`,
-    element: <GraphRoute />
-  },
-  {
-    path: `/graph/node/namespaces/:namespace/${Paths.SERVICES}/:service`,
-    element: <GraphRoute />
-  },
-  {
-    path: `/graph/node/namespaces/:namespace/${Paths.WORKLOADS}/:workload`,
-    element: <GraphRoute />
-  },
-  {
-    path: '/graph/namespaces',
-    element: <GraphPage />
-  },
   {
     path: `/graphpf/node/namespaces/:namespace/${Paths.AGGREGATES}/:aggregate/:aggregateValue`,
     element: <GraphRoutePF />
diff --git a/frontend/src/routes/GraphRoute.tsx b/frontend/src/routes/GraphRoute.tsx
deleted file mode 100644
index ea441d8e5..000000000
--- a/frontend/src/routes/GraphRoute.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from 'react';
-import { useParams } from 'react-router-dom-v5-compat';
-import { GraphPage, GraphURLPathProps } from 'pages/Graph/GraphPage';
-
-/**
- * Graph wrapper to add routing parameters to GraphPage
- * Some platforms where Kiali is deployed reuse GraphPage but
- * do not work with react-router params (like Openshift Console)
- */
-export const GraphRoute: React.FC = () => {
-  const { aggregate, aggregateValue, app, namespace, service, version, workload } = useParams<GraphURLPathProps>();
-
-  return (
-    <GraphPage
-      aggregate={aggregate}
-      aggregateValue={aggregateValue}
-      app={app}
-      namespace={namespace}
-      service={service}
-      version={version}
-      workload={workload}
-    ></GraphPage>
-  );
-};
diff --git a/frontend/src/routes/GraphRoutePF.tsx b/frontend/src/routes/GraphRoutePF.tsx
index 96b237ce4..c705a9354 100644
--- a/frontend/src/routes/GraphRoutePF.tsx
+++ b/frontend/src/routes/GraphRoutePF.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { useParams } from 'react-router-dom-v5-compat';
-import { GraphPagePF, GraphURLPathProps } from 'pages/GraphPF/GraphPagePF';
+import { GraphPagePF, GraphURLPathProps } from 'pages/Graph/GraphPagePF';
 
 export const GraphRoutePF: React.FC = () => {
   const { aggregate, aggregateValue, app, namespace, service, version, workload } = useParams<GraphURLPathProps>();
diff --git a/frontend/src/services/Api.ts b/frontend/src/services/Api.ts
index 8cd5d48a0..e0bd6cb36 100644
--- a/frontend/src/services/Api.ts
+++ b/frontend/src/services/Api.ts
@@ -50,7 +50,7 @@ import { ComponentStatus, IstiodResourceThresholds } from '../types/IstioStatus'
 import { TracingInfo, TracingResponse, TracingSingleResponse } from '../types/TracingInfo';
 import { ControlPlane, MeshDefinition, MeshQuery } from '../types/Mesh';
 import { DashboardQuery, IstioMetricsOptions, MetricsStatsQuery } from '../types/MetricsOptions';
-import { IstioMetricsMap, MetricsPerNamespace, MetricsStatsResult, ZtunnelMetricsMap } from '../types/Metrics';
+import { IstioMetricsMap, MetricsPerNamespace, MetricsStatsResult, ResourceUsageMetricsMap } from '../types/Metrics';
 import { Namespace } from '../types/Namespace';
 import { KialiCrippledFeatures, ServerConfig } from '../types/ServerConfig';
 import { StatusState } from '../types/StatusState';
@@ -240,21 +240,21 @@ export const getControlPlaneMetrics = (
   );
 };
 
-export const getZtunnelMetrics = (
+export const getResourceUsageMetrics = (
   namespace: string,
   workload: string,
   params: IstioMetricsOptions,
   cluster?: string
-): Promise<ApiResponse<Readonly<ZtunnelMetricsMap>>> => {
+): Promise<ApiResponse<Readonly<ResourceUsageMetricsMap>>> => {
   const queryParams: QueryParams<IstioMetricsOptions> = { ...params };
 
   if (cluster) {
     queryParams.clusterName = cluster;
   }
 
-  return newRequest<Readonly<ZtunnelMetricsMap>>(
+  return newRequest<Readonly<ResourceUsageMetricsMap>>(
     HTTP_VERBS.GET,
-    urls.ztunnelMetrics(namespace, workload),
+    urls.resourceUsageMetrics(namespace, workload),
     queryParams,
     {}
   );
diff --git a/frontend/src/store/Selectors/GraphData.ts b/frontend/src/store/Selectors/GraphData.ts
index 6696546f3..fe5b22961 100644
--- a/frontend/src/store/Selectors/GraphData.ts
+++ b/frontend/src/store/Selectors/GraphData.ts
@@ -10,10 +10,10 @@ import {
   hasProtocolTraffic
 } from '../../types/Graph';
 import { isIstioNamespace } from '../../config/ServerConfig';
-import { toSafeCyFieldName } from 'components/CytoscapeGraph/CytoscapeGraphUtils';
 import { AppHealth, NA, ServiceHealth, WorkloadHealth } from 'types/Health';
+import { toSafeFieldName } from 'helpers/GraphHelpers';
 
-// When updating the cytoscape graph, the element data expects to have all the changes
+// When updating the graph, the element data expects to have all the changes.
 // non-provided values are taken as "this didn't change", similar as setState does.
 // Put default values for all fields that are omitted.
 export const decorateGraphData = (graphData: GraphElements, duration: number): DecoratedGraphElements => {
@@ -144,7 +144,7 @@ export const decorateGraphData = (graphData: GraphElements, duration: number): D
           decoratedNode.data.labels = undefined;
           const prefixedLabels: { [key: string]: string } = {};
           for (const key in labels) {
-            prefixedLabels[toSafeCyFieldName(`label:${key}`)] = labels[key];
+            prefixedLabels[toSafeFieldName(`label:${key}`)] = labels[key];
           }
           decoratedNode.data = { ...prefixedLabels, ...decoratedNode.data };
         }
diff --git a/frontend/src/store/Store.ts b/frontend/src/store/Store.ts
index b50c47596..3455d1009 100644
--- a/frontend/src/store/Store.ts
+++ b/frontend/src/store/Store.ts
@@ -13,7 +13,6 @@ import {
   EdgeMode,
   GraphDefinition,
   GraphType,
-  Layout,
   NodeParamsType,
   RankMode,
   RankResult,
@@ -28,6 +27,8 @@ import { TracingState } from 'reducers/TracingState';
 import { MetricsStatsState } from 'reducers/MetricsStatsState';
 import { CertsInfo } from 'types/CertsInfo';
 import { MeshCluster, MeshDefinition, MeshTarget } from '../types/Mesh';
+import { GraphLayout } from 'pages/Graph/GraphPF';
+import { MeshLayout } from 'pages/Mesh/layouts/layoutFactory';
 
 // Store is the Redux Data store
 
@@ -96,8 +97,8 @@ export interface MessageCenterState {
 export interface GraphState {
   edgeMode: EdgeMode;
   graphDefinition: GraphDefinition | null; // Not for consumption. Only for "Debug" dialog.
-  layout: Layout;
-  namespaceLayout: Layout;
+  layout: GraphLayout;
+  namespaceLayout: GraphLayout;
   node?: NodeParamsType;
   rankResult: RankResult;
   summaryData: SummaryData | null;
@@ -142,7 +143,7 @@ export interface MeshToolbarState {
 
 export interface MeshState {
   definition: MeshDefinition | null;
-  layout: Layout;
+  layout: MeshLayout;
   target: MeshTarget | null;
   toolbarState: MeshToolbarState;
   updateTime: TimeInMilliseconds;
diff --git a/frontend/src/types/ErrorRate/GraphEdgeStatus.ts b/frontend/src/types/ErrorRate/GraphEdgeStatus.ts
index c95bd6060..e8b6d15aa 100644
--- a/frontend/src/types/ErrorRate/GraphEdgeStatus.ts
+++ b/frontend/src/types/ErrorRate/GraphEdgeStatus.ts
@@ -1,62 +1,9 @@
 import { ToleranceConfig } from '../ServerConfig';
-import {
-  ascendingThresholdCheck,
-  ThresholdStatus,
-  RATIO_NA,
-  HEALTHY,
-  NA,
-  RequestType,
-  FAILURE,
-  DEGRADED
-} from '../Health';
-import {
-  CytoscapeGlobalScratchNamespace,
-  DecoratedGraphEdgeData,
-  DecoratedGraphNodeData,
-  Responses,
-  TrafficRate
-} from '../Graph';
+import { ascendingThresholdCheck, ThresholdStatus, RATIO_NA, HEALTHY, NA, RequestType } from '../Health';
+import { DecoratedGraphEdgeData, DecoratedGraphNodeData, Responses } from '../Graph';
 import { aggregate, checkExpr, getRateHealthConfig, transformEdgeResponses } from './utils';
 import { RequestTolerance } from './types';
 import { RateHealth } from '../HealthAnnotation';
-import { decoratedEdgeData, decoratedNodeData } from 'components/CytoscapeGraph/CytoscapeGraphUtils';
-
-export const assignEdgeHealth = (cy: any) => {
-  const cyGlobal = cy.scratch(CytoscapeGlobalScratchNamespace);
-
-  cy.edges().forEach(ele => {
-    const edgeData = decoratedEdgeData(ele);
-
-    if (!edgeData.hasTraffic) {
-      return;
-    }
-    if (edgeData.protocol === 'tcp') {
-      return;
-    }
-    if (edgeData.protocol === 'grpc' && !cyGlobal.trafficRates.includes(TrafficRate.GRPC_REQUEST)) {
-      return;
-    }
-
-    const sourceNodeData = decoratedNodeData(ele.source());
-    const destNodeData = decoratedNodeData(ele.target());
-    const statusEdge = getEdgeHealth(edgeData, sourceNodeData, destNodeData);
-
-    switch (statusEdge.status) {
-      case FAILURE:
-        edgeData.healthStatus = FAILURE.name;
-        return;
-      case DEGRADED:
-        edgeData.healthStatus = DEGRADED.name;
-        return;
-      case HEALTHY:
-        edgeData.healthStatus = HEALTHY.name;
-        return;
-      default:
-        edgeData.healthStatus = NA.name;
-        return;
-    }
-  });
-};
 
 /*
  Return the status for the edge from source to target
diff --git a/frontend/src/types/Graph.ts b/frontend/src/types/Graph.ts
index 55c2972ed..f1c8d34ce 100644
--- a/frontend/src/types/Graph.ts
+++ b/frontend/src/types/Graph.ts
@@ -2,17 +2,13 @@ import { Namespace } from './Namespace';
 import { AppenderString, DurationInSeconds, TimeInSeconds } from './Common';
 import { Health } from './Health';
 import { HealthAnnotationType } from './HealthAnnotation';
-
-export interface Layout {
-  name: string;
-}
+import { Controller, GraphElement } from '@patternfly/react-topology';
 
 export const SUMMARY_PANEL_CHART_WIDTH = 250;
 
 export type SummaryType = 'graph' | 'node' | 'edge' | 'box';
 
 export interface SummaryData {
-  isPF?: boolean;
   summaryTarget: any;
   summaryType: SummaryType;
 }
@@ -238,31 +234,11 @@ export interface NodeParamsType {
   workload: string;
 }
 
-// This data is stored in the _global scratch area in the cy graph
-// for use by code that needs access to it.
-// We can add more props to this scratch data as the need arises.
-export const CytoscapeGlobalScratchNamespace = '_global';
-export type CytoscapeGlobalScratchData = {
-  activeNamespaces: Namespace[];
-  edgeLabels: EdgeLabelMode[];
-  forceLabels: boolean;
-  graphType: GraphType;
-  homeCluster: string;
-  showOutOfMesh: boolean;
-  showSecurity: boolean;
-  showVirtualServices: boolean;
-  trafficRates: TrafficRate[];
-};
-
-export interface CytoscapeBaseEvent {
-  summaryTarget: any; // the cytoscape element that was the target of the event
+export interface GraphEvent {
+  summaryTarget: GraphElement | Controller; // the target of the event
   summaryType: SummaryType; // what the summary panel should show
 }
 
-export interface GraphEvent extends CytoscapeBaseEvent {
-  isPF?: boolean;
-}
-
 // Graph Structures
 type PercentageOfTrafficByFlag = {
   [flag: string]: string;
diff --git a/frontend/src/types/IstioObjects.ts b/frontend/src/types/IstioObjects.ts
index 75f6d72d2..8b006684b 100644
--- a/frontend/src/types/IstioObjects.ts
+++ b/frontend/src/types/IstioObjects.ts
@@ -3,8 +3,8 @@ import { ServicePort } from './ServiceInfo';
 import { ProxyStatus } from './Health';
 import { TimeInSeconds } from './Common';
 import { KIALI_RELATED_LABEL, KIALI_WIZARD_LABEL } from 'components/IstioWizards/WizardActions';
-import { PFColorVal } from 'components/Pf/PfColors';
 import { TypeMeta } from './Kubernetes';
+import { PFColors } from 'components/Pf/PfColors';
 
 // Common types
 
@@ -258,7 +258,7 @@ export type AccessLog = {
 
 export type LogEntry = {
   accessLog?: AccessLog;
-  color?: PFColorVal;
+  color?: PFColors;
   message: string;
   severity: string;
   timestamp: string;
diff --git a/frontend/src/types/Metrics.ts b/frontend/src/types/Metrics.ts
index 0d3691afc..dab3e880f 100644
--- a/frontend/src/types/Metrics.ts
+++ b/frontend/src/types/Metrics.ts
@@ -25,6 +25,11 @@ export type ZtunnelMetricsMap = {
   ztunnel_workload_manager?: Metric[];
 };
 
+export type ResourceUsageMetricsMap = {
+  cpu_usage?: Metric[];
+  memory_usage?: Metric[];
+};
+
 export type MetricsPerNamespace = { [key: string]: IstioMetricsMap };
 
 export type IstioMetricsMap = {
diff --git a/frontend/src/types/ServerConfig.ts b/frontend/src/types/ServerConfig.ts
index 41ce2d7f4..bf9454396 100644
--- a/frontend/src/types/ServerConfig.ts
+++ b/frontend/src/types/ServerConfig.ts
@@ -38,15 +38,11 @@ interface GraphTraffic {
 
 interface GraphSettings {
   animation: 'point' | 'dash';
-  fontLabel: number;
-  minFontBadge: number;
-  minFontLabel: number;
 }
 
 interface GraphUIDefaults {
   findOptions: GraphFindOption[];
   hideOptions: GraphFindOption[];
-  impl: 'both' | 'cy' | 'pf';
   settings: GraphSettings;
   traffic: GraphTraffic;
 }
diff --git a/frontend/src/utils/GraphUtils.ts b/frontend/src/utils/GraphUtils.ts
index e42ba1e4d..aa5106fbc 100644
--- a/frontend/src/utils/GraphUtils.ts
+++ b/frontend/src/utils/GraphUtils.ts
@@ -1,6 +1,6 @@
-import { LayoutName } from 'pages/GraphPF/GraphPF';
+import { GraphLayout } from 'pages/Graph/GraphPF';
 
 // check if the graph layout supports grouping
-export const supportsGroups = (layoutName: LayoutName): boolean => {
-  return layoutName === LayoutName.Dagre;
+export const supportsGroups = (layoutName: GraphLayout): boolean => {
+  return layoutName === GraphLayout.Dagre;
 };
diff --git a/frontend/src/utils/TrafficRate.ts b/frontend/src/utils/TrafficRate.ts
index 7134075d2..7946c0b06 100644
--- a/frontend/src/utils/TrafficRate.ts
+++ b/frontend/src/utils/TrafficRate.ts
@@ -1,3 +1,4 @@
+import { GraphElement } from '@patternfly/react-topology';
 import _ from 'lodash';
 import { EdgeAttr, NodeAttr } from 'types/Graph';
 
@@ -26,27 +27,23 @@ export interface TrafficRateGrpc {
   rateNoResponse: number;
 }
 
-const data = (elem: any, prop: string, isPF: boolean) => {
-  return isPF ? elem.getData()[prop] : elem.data(prop);
+const data = (elem: GraphElement, prop: string) => {
+  return elem.getData()[prop];
 };
 
-export const getTrafficRateGrpc = (
-  element: any,
-  isPF: boolean = false,
-  trafficType: TRAFFIC_GRPC = NODE_GRPC_IN
-): TrafficRateGrpc => {
+export const getTrafficRateGrpc = (element: any, trafficType: TRAFFIC_GRPC = NODE_GRPC_IN): TrafficRateGrpc => {
   return {
-    rate: safeRate(data(element, trafficType.RATE, isPF)),
-    rateGrpcErr: safeRate(data(element, trafficType.RATEGRPCERR, isPF)),
-    rateNoResponse: safeRate(data(element, trafficType.RATENORESPONSE, isPF))
+    rate: safeRate(data(element, trafficType.RATE)),
+    rateGrpcErr: safeRate(data(element, trafficType.RATEGRPCERR)),
+    rateNoResponse: safeRate(data(element, trafficType.RATENORESPONSE))
   };
 };
 
-export const getAccumulatedTrafficRateGrpc = (elements: any, isPF: boolean = false): TrafficRateGrpc => {
+export const getAccumulatedTrafficRateGrpc = (elements: any): TrafficRateGrpc => {
   return _.reduce(
     elements,
     (r: TrafficRateGrpc, element): TrafficRateGrpc => {
-      const elementTrafficRate = getTrafficRateGrpc(element, isPF, EDGE_GRPC);
+      const elementTrafficRate = getTrafficRateGrpc(element, EDGE_GRPC);
       r.rate += elementTrafficRate.rate;
       r.rateGrpcErr += elementTrafficRate.rateGrpcErr;
       r.rateNoResponse += elementTrafficRate.rateNoResponse;
@@ -87,25 +84,21 @@ export interface TrafficRateHttp {
   rateNoResponse: number;
 }
 
-export const getTrafficRateHttp = (
-  element: any,
-  isPF: boolean = false,
-  trafficType: TRAFFIC_HTTP = NODE_HTTP_IN
-): TrafficRateHttp => {
+export const getTrafficRateHttp = (element: any, trafficType: TRAFFIC_HTTP = NODE_HTTP_IN): TrafficRateHttp => {
   return {
-    rate: safeRate(data(element, trafficType.RATE, isPF)),
-    rate3xx: safeRate(data(element, trafficType.RATE3XX, isPF)),
-    rate4xx: safeRate(data(element, trafficType.RATE4XX, isPF)),
-    rate5xx: safeRate(data(element, trafficType.RATE5XX, isPF)),
-    rateNoResponse: safeRate(data(element, trafficType.RATENORESPONSE, isPF))
+    rate: safeRate(data(element, trafficType.RATE)),
+    rate3xx: safeRate(data(element, trafficType.RATE3XX)),
+    rate4xx: safeRate(data(element, trafficType.RATE4XX)),
+    rate5xx: safeRate(data(element, trafficType.RATE5XX)),
+    rateNoResponse: safeRate(data(element, trafficType.RATENORESPONSE))
   };
 };
 
-export const getAccumulatedTrafficRateHttp = (elements, isPF: boolean = false): TrafficRateHttp => {
+export const getAccumulatedTrafficRateHttp = (elements): TrafficRateHttp => {
   return _.reduce(
     elements,
     (r: TrafficRateHttp, element): TrafficRateHttp => {
-      const elementTrafficRate = getTrafficRateHttp(element, isPF, EDGE_HTTP);
+      const elementTrafficRate = getTrafficRateHttp(element, EDGE_HTTP);
       r.rate += elementTrafficRate.rate;
       r.rate3xx += elementTrafficRate.rate3xx;
       r.rate4xx += elementTrafficRate.rate4xx;
@@ -132,21 +125,17 @@ export interface TrafficRateTcp {
   rate: number;
 }
 
-export const getTrafficRateTcp = (
-  element: any,
-  isPF: boolean = false,
-  trafficType: TRAFFIC_TCP = NODE_TCP_IN
-): TrafficRateTcp => {
+export const getTrafficRateTcp = (element: any, trafficType: TRAFFIC_TCP = NODE_TCP_IN): TrafficRateTcp => {
   return {
-    rate: safeRate(data(element, trafficType.RATE, isPF))
+    rate: safeRate(data(element, trafficType.RATE))
   };
 };
 
-export const getAccumulatedTrafficRateTcp = (elements: any, isPF: boolean = false): TrafficRateTcp => {
+export const getAccumulatedTrafficRateTcp = (elements: any): TrafficRateTcp => {
   return _.reduce(
     elements,
     (r: TrafficRateTcp, element): TrafficRateTcp => {
-      const elementTrafficRate = getTrafficRateTcp(element, isPF, EDGE_TCP);
+      const elementTrafficRate = getTrafficRateTcp(element, EDGE_TCP);
       r.rate += elementTrafficRate.rate;
       return r;
     },
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 6becc05a5..8023eec08 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2671,11 +2671,6 @@
   resolved "https://registry.yarnpkg.com/@types/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-4.0.5.tgz#5c4d791e9b1f5928bcf179a496538193cb4298b4"
   integrity sha512-5+eM+RWNOW7X9JMQ4auux/HxTWxBK+Mz2wStEnnbuJr0HRLqCNJDUDvkeLtob8X74uA3L0bCFNyC2WotfJtM6Q==
 
-"@types/cytoscape@3.14.0":
-  version "3.14.0"
-  resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.14.0.tgz#346b5430a7a1533784bcf44fcbe6c5255b948d36"
-  integrity sha512-qOhhZFGb35457vmCu9YHqwjqleraSAEikaIFmMUPWzTC7FrwkS7VR/6ymqRHW7Z8wivHuzsZDYCoePIepd69qg==
-
 "@types/d3-array@*", "@types/d3-array@^3.0.3":
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.5.tgz#857c1afffd3f51319bbc5b301956aca68acaa7b8"
@@ -5103,11 +5098,6 @@ csstype@3.0.10, csstype@^3.0.2:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
   integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
 
-cy-node-html-label@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/cy-node-html-label/-/cy-node-html-label-2.0.0.tgz#822c0c8afc88b4ba05268544289d0b367ce21e3b"
-  integrity sha512-hjvXlYAXaOjRryy8D3fHaeoRUVsgFi3589rcMtQRky08Ok+y7Lucqh895pNLJLNmXGm2N7083b8sPkPdhP3pag==
-
 cypress-multi-reporters@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.0.tgz#2c6833b92e3df412c233657c55009e2d5e1cc7c1"
@@ -5172,26 +5162,6 @@ cypress@^13.6.1:
     untildify "^4.0.0"
     yauzl "^2.10.0"
 
-cytoscape-canvas@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/cytoscape-canvas/-/cytoscape-canvas-3.0.1.tgz#7d963be51e6e5f3f2482b05d22562c8ee941d125"
-  integrity sha512-R1nCLnJHGTR5fWEpNQQBPyoigdnfhnk1lOHegzmzz1Kg6yxzVf0hfdRAMa0wPAhtC4kkgyoViiIfZ/zyCqMhEQ==
-
-cytoscape-popper@1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/cytoscape-popper/-/cytoscape-popper-1.0.7.tgz#8154ff507d0cc1a17952f00643e71fc1f8ea9fae"
-  integrity sha512-b/vfoBL2u9GU+J7eceGT9cZ4hctn/ZzV7PXq4w6fk+45qQuOHtlhpP3WxGKpBXNqENhk7Dk0BwlIYROjYQmidQ==
-  dependencies:
-    popper.js "^1.0.0"
-
-cytoscape@3.15.5:
-  version "3.15.5"
-  resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.15.5.tgz#34f4d5e54ffadbbbd044d5c041f17d251e741748"
-  integrity sha512-wkF2zkwvbBvT3Gj9ZVYhxqx6TvyZIuDx7Dhdy8m0z4QmDOXteMz+9oUgV+1btaBxA1VePZRUpoz1lwJixAyyYw==
-  dependencies:
-    heap "^0.2.6"
-    lodash.debounce "^4.0.8"
-
 "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.1.6, d3-array@^3.2.0:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
@@ -7374,11 +7344,6 @@ he@^1.2.0:
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-heap@^0.2.6:
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac"
-  integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw=
-
 heimdalljs-logger@^0.1.10, heimdalljs-logger@^0.1.7:
   version "0.1.10"
   resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz#90cad58aabb1590a3c7e640ddc6a4cd3a43faaf7"
@@ -9887,7 +9852,7 @@ point-in-svg-path@^1.0.1:
   resolved "https://registry.yarnpkg.com/point-in-svg-path/-/point-in-svg-path-1.0.2.tgz#e7b25370e96f58a858c34882ffb57d41b7a574bf"
   integrity sha512-+Smsf7B9e7eRFHIwpN+4rE8inF2APbFWeywPfUgbeh02xdJSkbTz6Pqdt7A36wVCR+CnLbaNkRnBjgOpF5RMVQ==
 
-popper.js@^1.0.0, popper.js@^1.14.4, popper.js@^1.14.6, popper.js@^1.16.1:
+popper.js@^1.14.4, popper.js@^1.14.6, popper.js@^1.16.1:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
   integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
@@ -12004,7 +11969,14 @@ stringify-object@^3.3.0:
     is-obj "^1.0.1"
     is-regexp "^1.0.0"
 
-strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -13521,6 +13493,15 @@ workerpool@^6.5.1:
   resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544"
   integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
 
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrap-ansi@^6.2.0:
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -13530,7 +13511,7 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrap-ansi@^7.0.0, "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+wrap-ansi@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
diff --git a/go.mod b/go.mod
index ee1916998..82030d8bb 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/kiali/kiali
 
-go 1.23.2
+go 1.23.6
 
 require (
 	github.com/Masterminds/semver/v3 v3.3.1
diff --git a/graph/api/api.go b/graph/api/api.go
index b9001fa1a..beea01ace 100644
--- a/graph/api/api.go
+++ b/graph/api/api.go
@@ -8,7 +8,7 @@ import (
 	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/graph"
-	"github.com/kiali/kiali/graph/config/cytoscape"
+	config_common "github.com/kiali/kiali/graph/config/common"
 	"github.com/kiali/kiali/graph/telemetry/istio"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/observability"
@@ -101,10 +101,11 @@ func generateGraph(trafficMap graph.TrafficMap, o graph.Options) (int, interface
 
 	var vendorConfig interface{}
 	switch o.ConfigVendor {
-	case graph.VendorCytoscape:
-		vendorConfig = cytoscape.NewConfig(trafficMap, o.ConfigOptions)
+	case graph.VendorCommon:
+		vendorConfig = config_common.NewConfig(trafficMap, o.ConfigOptions)
 	default:
-		graph.Error(fmt.Sprintf("ConfigVendor [%s] not supported", o.ConfigVendor))
+		vendorConfig = config_common.NewConfig(trafficMap, o.ConfigOptions)
+		log.Debugf("ConfigVendor [%s] not supported, defaulting to [Common]", o.ConfigVendor)
 	}
 
 	log.Tracef("Done generating config for [%s] graph", o.ConfigVendor)
diff --git a/graph/config.go b/graph/config.go
index 64273a575..a8e2739ac 100644
--- a/graph/config.go
+++ b/graph/config.go
@@ -5,6 +5,6 @@ type ConfigVendor interface {
 
 	// NewConfig is required by the ConfigVendor interface.  It must produce a valid
 	// Config for the provided TrafficMap, It is recommended to use the graph/util.go
-	// definitions for error handling. Refer to the Cytoscape implementation as an example.
+	// definitions for error handling. Refer to the Default implementation as an example.
 	NewConfig(trafficMap TrafficMap, o ConfigOptions) interface{}
 }
diff --git a/graph/config/cytoscape/cytoscape.go b/graph/config/common/common.go
similarity index 96%
rename from graph/config/cytoscape/cytoscape.go
rename to graph/config/common/common.go
index 868129aed..01da18af8 100644
--- a/graph/config/cytoscape/cytoscape.go
+++ b/graph/config/common/common.go
@@ -1,19 +1,15 @@
-// Package cytoscape provides conversion from our graph to the CystoscapeJS
-// configuration json model.
-//
-// The following links are useful for understanding CytoscapeJS and it's configuration:
-//
-// Main page:   http://js.cytoscape.org/
-// JSON config: http://js.cytoscape.org/#notation/elements-json
-// Demos:       http://js.cytoscape.org/#demos
+// Package "common" provides conversion from our graph structure to a common JSON provided back to clients.
+// This is likely the only JSON format that will be needed, although the architecture provides fo multiple providers.
+// This format was originally built for CystoscapeJS, but then was fine for our Patternfly Topology configuration
+// json model.
 //
 // Algorithm: Process the graph structure adding nodes and edges, decorating each
-//            with information provided.  An optional second pass generates compound
-//            nodes for requested boxing.
 //
-// The package provides the Cytoscape implementation of graph/ConfigVendor.
-
-package cytoscape
+//	with information provided.  An optional second pass generates box
+//	nodes for requested boxing.
+//
+// The package provides the Common implementation of graph/ConfigVendor.
+package common
 
 import (
 	"crypto/sha256"
@@ -83,11 +79,8 @@ type VSInfo struct {
 type HealthConfig map[string]string
 
 type NodeData struct {
-	// Cytoscape Fields
-	ID     string `json:"id"`               // unique internal node ID (n0, n1...)
-	Parent string `json:"parent,omitempty"` // Compound Node parent ID
-
-	// App Fields (not required by Cytoscape)
+	ID                    string              `json:"id"`               // unique internal node ID (n0, n1...)
+	Parent                string              `json:"parent,omitempty"` // Compound Node parent ID
 	NodeType              string              `json:"nodeType"`
 	Cluster               string              `json:"cluster"`
 	Namespace             string              `json:"namespace"`
@@ -132,12 +125,9 @@ type WaypointEdge struct {
 }
 
 type EdgeData struct {
-	// Cytoscape Fields
-	ID     string `json:"id"`     // unique internal edge ID (e0, e1...)
-	Source string `json:"source"` // parent node ID
-	Target string `json:"target"` // child node ID
-
-	// App Fields (not required by Cytoscape)
+	ID              string          `json:"id"`                        // unique internal edge ID (e0, e1...)
+	Source          string          `json:"source"`                    // parent node ID
+	Target          string          `json:"target"`                    // child node ID
 	DestPrincipal   string          `json:"destPrincipal,omitempty"`   // principal used for the edge destination
 	IsMTLS          string          `json:"isMTLS,omitempty"`          // set to the percentage of traffic using a mutual TLS connection
 	ResponseTime    string          `json:"responseTime,omitempty"`    // in millis
diff --git a/graph/options.go b/graph/options.go
index 85237c68b..bfb41aec8 100644
--- a/graph/options.go
+++ b/graph/options.go
@@ -21,9 +21,9 @@ import (
 
 // The supported vendors
 const (
-	VendorCytoscape        string = "cytoscape"
+	VendorCommon           string = "common"
 	VendorIstio            string = "istio"
-	defaultConfigVendor    string = VendorCytoscape
+	defaultConfigVendor    string = VendorCommon
 	defaultTelemetryVendor string = VendorIstio
 )
 
@@ -179,7 +179,7 @@ func NewOptions(r *net_http.Request, namespacesService *business.NamespaceServic
 	}
 	if configVendor == "" {
 		configVendor = defaultConfigVendor
-	} else if configVendor != VendorCytoscape {
+	} else if configVendor != VendorCommon {
 		BadRequest(fmt.Sprintf("Invalid configVendor [%s]", configVendor))
 	}
 	if durationString == "" {
diff --git a/graph/telemetry/istio/istio.go b/graph/telemetry/istio/istio.go
index 6a8a72f96..5c4b01f04 100644
--- a/graph/telemetry/istio/istio.go
+++ b/graph/telemetry/istio/istio.go
@@ -16,7 +16,7 @@ package istio
 //   Step 2) For the global traffic map
 //     a) Apply standard and requested finalizers to alter or append-to the final traffic-map
 //
-//     b) Convert the final traffic-map to the requested vendor configiration (i.e. Cytoscape) and return
+//     b) Convert the final traffic-map to the requested vendor configuration (i.e. Common) and return
 //
 // Supports three vendor-specific query parameters:
 //   aggregate: Must be a valid metric attribute (default: request_operation)
diff --git a/hack/istio/ambient/install-waypoints.sh b/hack/istio/ambient/install-waypoints.sh
index e54e30cf0..d172ceb81 100755
--- a/hack/istio/ambient/install-waypoints.sh
+++ b/hack/istio/ambient/install-waypoints.sh
@@ -100,6 +100,8 @@ ${ISTIOCTL} waypoint apply -n waypoint-forservice --enroll-namespace
 
 # Create a waypoint for workload and send requests to pod b
 ${CLIENT_EXE} apply -f ${HACK_SCRIPT_DIR}/echo-service.yaml -n waypoint-forworkload
+${CLIENT_EXE} wait --for=condition=Ready pod/echo-server -n waypoint-forworkload --timeout=60s
+
 sleep 15
 # Update with echo-server IP
 POD_IP=$($CLIENT_EXE get pod echo-server -n waypoint-forworkload -o jsonpath="{.status.podIP}")
diff --git a/hack/istio/functions.sh b/hack/istio/functions.sh
index 361528a47..16a641e59 100644
--- a/hack/istio/functions.sh
+++ b/hack/istio/functions.sh
@@ -41,3 +41,29 @@ is_istio_version_eq_greater_than() {
   fi
 
 }
+
+# Returns 0 if the istio version is greater than specified, 0 otherwise, using two arguments
+is_istio_version_eq_greater_than_expected() {
+  local expected_version=$1
+  local istio_version=$2
+
+  istio_expected_version=$(echo "$expected_version" | cut -d'-' -f1)
+
+  IFS='.' read -r major minor patch <<< "$istio_version"
+  IFS='.' read -r major_expected minor_expected patch_expected <<< "$istio_expected_version"
+  IFS=' '
+  if [ "${major}" -lt "${major_expected}" ]; then
+    return 1
+  else
+    if [ "${major}" -eq "${major_expected}" ]; then
+      if [ "${minor}" -lt "${minor_expected}" ]; then
+        return 0
+      else
+        return 1
+      fi
+    else
+      return 0
+    fi
+  fi
+
+}
\ No newline at end of file
diff --git a/hack/istio/install-testing-demos.sh b/hack/istio/install-testing-demos.sh
index be734c9fe..0cfa9ea7e 100755
--- a/hack/istio/install-testing-demos.sh
+++ b/hack/istio/install-testing-demos.sh
@@ -192,6 +192,13 @@ EOF
 
     echo "Deploying sleep demo ..."
     "${SCRIPT_DIR}/install-sleep-demo.sh" -c kubectl -in ${ISTIO_NAMESPACE} -a ${ARCH} ${AMBIENT_ARGS_BOOKINFO}
+
+    echo "Deploying waypoint proxies ..."
+    "${SCRIPT_DIR}/ambient/install-waypoints.sh"
+
+    echo "Deploying sidecar-ambient ..."
+    "${SCRIPT_DIR}/ambient/install-sidecars-ambient.sh"
+
   else
     echo "Deploying bookinfo demo..."
     "${SCRIPT_DIR}/install-bookinfo-demo.sh" -c kubectl -mp ${MINIKUBE_PROFILE} -tg -in ${ISTIO_NAMESPACE} -a ${ARCH} ${gateway_yaml:+-g ${gateway_yaml}} ${AMBIENT_ARGS_BOOKINFO}
diff --git a/hack/run-integration-tests.sh b/hack/run-integration-tests.sh
index bc9c261fb..2dc106841 100755
--- a/hack/run-integration-tests.sh
+++ b/hack/run-integration-tests.sh
@@ -146,6 +146,7 @@ fi
 
 # Determine where this script is and make it the cwd
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
+source ${SCRIPT_DIR}/istio/functions.sh
 
 # This is used in multiple places and you need to call 'setKialiURL' first.
 KIALI_URL=""
@@ -380,6 +381,7 @@ elif [ "${TEST_SUITE}" == "${FRONTEND}" ]; then
   yarn run cypress:run
   detectRaceConditions
 elif [ "${TEST_SUITE}" == "${FRONTEND_AMBIENT}" ]; then
+
   ensureCypressInstalled
   ensureKialiTracesReady "true"
 
@@ -404,8 +406,25 @@ elif [ "${TEST_SUITE}" == "${FRONTEND_AMBIENT}" ]; then
   fi
 
   cd "${SCRIPT_DIR}"/../frontend
-  yarn run cypress:run:ambient
+
+  # TODO: Remove when no support for Istio 1.23 is required
+  # Replace by "yarn run cypress:run:ambient"
+  if [ "${ISTIO_VERSION}" != "" ]; then
+    set +e
+    is_istio_version_eq_greater_than_expected "1.24.0" "${ISTIO_VERSION}"
+    status=$?
+    if [ "$status" -eq 0 ]; then
+      yarn run cypress:run:ambient123
+    else
+      yarn run cypress:run:ambient
+    fi
+    set -e
+  else
+    yarn run cypress:run:ambient
+  fi
+
   detectRaceConditions
+
 elif [ "${TEST_SUITE}" == "${FRONTEND_PRIMARY_REMOTE}" ]; then
   ensureCypressInstalled
   
diff --git a/handlers/README.md b/handlers/README.md
new file mode 100644
index 000000000..32ee05676
--- /dev/null
+++ b/handlers/README.md
@@ -0,0 +1,58 @@
+# Handlers
+
+Handlers in Kiali are associated with a particular Route defined in [routes.go](../routing/routes.go). When a request matches the URL pattern for a Route, the associated Handler is called. For example, when the router receives a request at `/api/clusters/health` the `ClustersHealth` handler is then called.
+
+Handlers must conform to the [net/http HandlerFunc](https://pkg.go.dev/net/http#HandlerFunc) signature. They typically create [business service(s)](../business/) _per request_ but they also need access to a number of objects that are initialized only once when the Kiali server first starts and then are reused across requests like the [config object](../config/config.go) or the [kiali cache](../kubernetes/cache/cache.go). In order to pass these cross request dependencies, handlers should be defined as closures, allowing the handler to be called once with the cross request dependencies when it is first defined in [routes.go](../routing/routes.go), and then invoked again repeatedly as a `HandlerFunc` when it is matched in the router.
+
+Example:
+```go
+routing/routes.go
+
+...
+{
+    "PizzaHandler",
+    "GET",
+    "/api/pizzas",
+    handlers.PizzaHandler(conf, cache),
+    true,
+},
+```
+
+```go
+handlers/pizza.go
+
+func PizzaHandler(conf *config.Config, cache cache.KialiCache) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+        clusterName := conf.KubernetesConfig.ClusterName
+        cache.GetPizzas(clusterName)
+        ...
+    }
+}
+```
+
+When writing a unit test you should use the actual handler. Aside from a few helper functions that simply reduce the amount of boilerplate you need to write, there is no magic and there are no global variables being used to test your handler.
+```go
+handlers/pizza_test.go
+
+func TestPizzaHandler(t *testing.T) {
+    conf := config.NewConfig()
+	k8s := kubetest.NewFakeK8sClient()
+	cf := kubetest.NewFakeClientFactoryWithClient(conf, k8s)
+	cache := cache.NewTestingCacheWithFactory(t, cf, *conf)
+	
+    pizzaHandler := WithFakeAuthInfo(conf, PizzaHandler(conf, cache))
+	mr := mux.NewRouter()
+	mr.HandleFunc("/api/pizzas", pizzaHandler)
+
+	ts := httptest.NewServer(mr)
+	url := ts.URL + "/api/pizzas"
+
+	resp, err := http.Get(url)
+	require.NoError(err)
+
+	actual, _ := io.ReadAll(resp.Body)
+
+	require.NotEmpty(actual)
+	assert.Equal(200, resp.StatusCode, string(actual))
+}
+```
\ No newline at end of file
diff --git a/handlers/apps.go b/handlers/apps.go
index 734faf109..663b3f5c4 100644
--- a/handlers/apps.go
+++ b/handlers/apps.go
@@ -9,7 +9,14 @@ import (
 	"github.com/gorilla/mux"
 
 	"github.com/kiali/kiali/business"
+	"github.com/kiali/kiali/config"
+	"github.com/kiali/kiali/grafana"
+	"github.com/kiali/kiali/istio"
+	"github.com/kiali/kiali/kubernetes"
+	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/models"
+	"github.com/kiali/kiali/prometheus"
+	"github.com/kiali/kiali/tracing"
 )
 
 // appParams holds the path and query parameters for appList and appDetails
@@ -46,89 +53,111 @@ func (p *appParams) extract(r *http.Request) {
 	}
 }
 
-// ClustersApps is the API handler to fetch all the apps to be displayed, related to a single cluster
-func ClustersApps(w http.ResponseWriter, r *http.Request) {
-	query := r.URL.Query()
-	namespacesQueryParam := query.Get("namespaces") // csl of namespaces
-	p := appParams{}
-	p.extract(r)
-
-	businessLayer, err := getBusiness(r)
-	if err != nil {
-		RespondWithError(w, http.StatusInternalServerError, "Apps initialization error: "+err.Error())
-		return
-	}
+// ClusterApps is the API handler to fetch all the apps to be displayed, related to a single cluster
+func ClusterApps(
+	conf *config.Config,
+	kialiCache cache.KialiCache,
+	clientFactory kubernetes.ClientFactory,
+	prom prometheus.ClientInterface,
+	cpm business.ControlPlaneMonitor,
+	traceClientLoader func() tracing.ClientInterface,
+	grafana *grafana.Service,
+	discovery *istio.Discovery,
+) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		query := r.URL.Query()
+		namespacesQueryParam := query.Get("namespaces") // csl of namespaces
+		p := appParams{}
+		p.extract(r)
+
+		businessLayer, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Apps initialization error: "+err.Error())
+			return
+		}
 
-	nss := []string{}
-	namespacesFromQueryParams := strings.Split(namespacesQueryParam, ",")
-	loadedNamespaces, _ := businessLayer.Namespace.GetClusterNamespaces(r.Context(), p.ClusterName)
-	for _, ns := range loadedNamespaces {
-		// If namespaces have been provided in the query, further filter the results to only include those namespaces.
-		if len(namespacesQueryParam) > 0 {
-			if slices.Contains(namespacesFromQueryParams, ns.Name) {
+		nss := []string{}
+		namespacesFromQueryParams := strings.Split(namespacesQueryParam, ",")
+		loadedNamespaces, _ := businessLayer.Namespace.GetClusterNamespaces(r.Context(), p.ClusterName)
+		for _, ns := range loadedNamespaces {
+			// If namespaces have been provided in the query, further filter the results to only include those namespaces.
+			if len(namespacesQueryParam) > 0 {
+				if slices.Contains(namespacesFromQueryParams, ns.Name) {
+					nss = append(nss, ns.Name)
+				}
+			} else {
+				// Otherwise no namespaces have been provided in the query params, so include all namespaces the user has access to.
 				nss = append(nss, ns.Name)
 			}
-		} else {
-			// Otherwise no namespaces have been provided in the query params, so include all namespaces the user has access to.
-			nss = append(nss, ns.Name)
 		}
-	}
-
-	clusterAppsList := &models.ClusterApps{
-		Apps:    []models.AppListItem{},
-		Cluster: p.ClusterName,
-	}
 
-	for _, ns := range nss {
-		criteria := business.AppCriteria{
-			Cluster: p.ClusterName, Namespace: ns, IncludeIstioResources: p.IncludeIstioResources,
-			IncludeHealth: p.IncludeHealth, RateInterval: p.RateInterval, QueryTime: p.QueryTime,
+		clusterAppsList := &models.ClusterApps{
+			Apps:    []models.AppListItem{},
+			Cluster: p.ClusterName,
 		}
 
-		if p.IncludeHealth {
-			rateInterval, err := adjustRateInterval(r.Context(), businessLayer, ns, p.RateInterval, p.QueryTime, p.ClusterName)
+		for _, ns := range nss {
+			criteria := business.AppCriteria{
+				Cluster: p.ClusterName, Namespace: ns, IncludeIstioResources: p.IncludeIstioResources,
+				IncludeHealth: p.IncludeHealth, RateInterval: p.RateInterval, QueryTime: p.QueryTime,
+			}
+
+			if p.IncludeHealth {
+				rateInterval, err := adjustRateInterval(r.Context(), businessLayer, ns, p.RateInterval, p.QueryTime, p.ClusterName)
+				if err != nil {
+					handleErrorResponse(w, err, "Adjust rate interval error: "+err.Error())
+					return
+				}
+				criteria.RateInterval = rateInterval
+			}
+
+			// Fetch and build apps
+			appList, err := businessLayer.App.GetClusterAppList(r.Context(), criteria)
 			if err != nil {
-				handleErrorResponse(w, err, "Adjust rate interval error: "+err.Error())
+				handleErrorResponse(w, err)
 				return
 			}
-			criteria.RateInterval = rateInterval
+			clusterAppsList.Apps = append(clusterAppsList.Apps, appList.Apps...)
 		}
 
-		// Fetch and build apps
-		appList, err := businessLayer.App.GetClusterAppList(r.Context(), criteria)
-		if err != nil {
-			handleErrorResponse(w, err)
-			return
-		}
-		clusterAppsList.Apps = append(clusterAppsList.Apps, appList.Apps...)
+		RespondWithJSON(w, http.StatusOK, clusterAppsList)
 	}
-
-	RespondWithJSON(w, http.StatusOK, clusterAppsList)
 }
 
 // AppDetails is the API handler to fetch all details to be displayed, related to a single app
-func AppDetails(w http.ResponseWriter, r *http.Request) {
-	p := appParams{}
-	p.extract(r)
+func AppDetails(
+	conf *config.Config,
+	kialiCache cache.KialiCache,
+	clientFactory kubernetes.ClientFactory,
+	prom prometheus.ClientInterface,
+	cpm business.ControlPlaneMonitor,
+	traceClientLoader func() tracing.ClientInterface,
+	grafana *grafana.Service,
+	discovery *istio.Discovery,
+) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		p := appParams{}
+		p.extract(r)
 
-	criteria := business.AppCriteria{
-		Namespace: p.Namespace, AppName: p.AppName, IncludeIstioResources: true, IncludeHealth: p.IncludeHealth,
-		RateInterval: p.RateInterval, QueryTime: p.QueryTime, Cluster: p.ClusterName,
-	}
+		criteria := business.AppCriteria{
+			Namespace: p.Namespace, AppName: p.AppName, IncludeIstioResources: true, IncludeHealth: p.IncludeHealth,
+			RateInterval: p.RateInterval, QueryTime: p.QueryTime, Cluster: p.ClusterName,
+		}
 
-	// Get business layer
-	business, err := getBusiness(r)
-	if err != nil {
-		RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
-		return
-	}
+		// Get business layer
+		business, err := getLayer(r, conf, kialiCache, clientFactory, cpm, prom, traceClientLoader, grafana, discovery)
+		if err != nil {
+			RespondWithError(w, http.StatusInternalServerError, "Services initialization error: "+err.Error())
+			return
+		}
 
-	// Fetch and build app
-	appDetails, err := business.App.GetAppDetails(r.Context(), criteria)
-	if err != nil {
-		handleErrorResponse(w, err)
-		return
-	}
+		// Fetch and build app
+		appDetails, err := business.App.GetAppDetails(r.Context(), criteria)
+		if err != nil {
+			handleErrorResponse(w, err)
+			return
+		}
 
-	RespondWithJSON(w, http.StatusOK, appDetails)
+		RespondWithJSON(w, http.StatusOK, appDetails)
+	}
 }
diff --git a/handlers/graph.go b/handlers/graph.go
index 0b2b69128..05acc6ef9 100644
--- a/handlers/graph.go
+++ b/handlers/graph.go
@@ -1,9 +1,9 @@
 package handlers
 
 // Graph.go provides handlers for graph request endpoints.   The handlers access vendor-specific
-// telemetry (default istio) and return vendor-specific configuration (default cytoscape). The
-// configuration format depends on the vendor but is typically JSON and provides what is necessary
-// to allow the vendor's tool to render the graph.
+// telemetry (default istio) and return vendor-specific configuration. The configuration format
+// depends on the vendor but is typically JSON and provides what is necessary to allow the vendor's
+// tool to render the graph.
 //
 // The algorithm is two-phased:
 //   Phase One: Generate a TrafficMap using the requested TelemetryVendor. This typically queries
@@ -18,7 +18,7 @@ package handlers
 //
 // The handlers accept the following query parameters (see notes below)
 //   appenders:       Comma-separated list of TelemetryVendor-specific appenders to run. (default: all)
-//   configVendor:    default: cytoscape
+//   configVendor:    default: common
 //   duration:        time.Duration indicating desired query range duration, (default: 10m)
 //   graphType:       Determines how to present the telemetry data. app | service | versionedApp | workload (default: workload)
 //   boxBy:           If supported by vendor, visually box by a specified node attribute (default: none)
diff --git a/handlers/metrics.go b/handlers/metrics.go
index 7ee033053..30b53e1f1 100644
--- a/handlers/metrics.go
+++ b/handlers/metrics.go
@@ -228,12 +228,13 @@ func ControlPlaneMetrics(promSupplier promClientSupplier) http.HandlerFunc {
 	}
 }
 
-// ZtunnelMetrics is the API handler to fetch metrics to be displayed, related to a single control plane revision
-func ZtunnelMetrics(promSupplier promClientSupplier) http.HandlerFunc {
+// ResourceUsageMetrics is the API handler to fetch metrics to be displayed, related to a single control plane revision
+func ResourceUsageMetrics(promSupplier promClientSupplier) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 
 		vars := mux.Vars(r)
 		namespace := vars["namespace"]
+		app := vars["app"]
 		cluster := clusterNameFromQuery(r.URL.Query())
 
 		metricsService, namespaceInfo := createMetricsServiceForNamespaceMC(w, r, promSupplier, namespace)
@@ -243,7 +244,7 @@ func ZtunnelMetrics(promSupplier promClientSupplier) http.HandlerFunc {
 		}
 		oldestNs := GetOldestNamespace(namespaceInfo)
 
-		params := models.IstioMetricsQuery{Cluster: cluster, Namespace: namespace}
+		params := models.IstioMetricsQuery{App: app, Cluster: cluster, Namespace: namespace}
 
 		err := extractIstioMetricsQueryParams(r, &params, oldestNs)
 		if err != nil {
@@ -258,7 +259,7 @@ func ZtunnelMetrics(promSupplier promClientSupplier) http.HandlerFunc {
 
 		metrics := make(models.MetricsMap)
 
-		ztunnelMetrics, err := metricsService.GetZtunnelMetrics(params)
+		ztunnelMetrics, err := metricsService.GetResourceMetrics(params)
 		if err != nil {
 			RespondWithError(w, http.StatusServiceUnavailable, err.Error())
 			return
@@ -358,10 +359,6 @@ func ClustersMetrics(promSupplier promClientSupplier) http.HandlerFunc {
 				return
 			}
 
-			if err != nil {
-				RespondWithError(w, http.StatusServiceUnavailable, err.Error())
-				return
-			}
 			result[namespace] = metrics
 		}
 
diff --git a/handlers/workloads.go b/handlers/workloads.go
index 14704d31a..89e827d24 100644
--- a/handlers/workloads.go
+++ b/handlers/workloads.go
@@ -335,10 +335,6 @@ func ConfigDumpZtunnel(w http.ResponseWriter, r *http.Request) {
 	pod := params["pod"]
 
 	dump := business.Workload.GetZtunnelConfig(cluster, namespace, pod)
-	if err != nil {
-		handleErrorResponse(w, err)
-		return
-	}
 
 	RespondWithJSON(w, http.StatusOK, dump)
 }
diff --git a/mesh/api/api.go b/mesh/api/api.go
index c28496eab..71e77930b 100644
--- a/mesh/api/api.go
+++ b/mesh/api/api.go
@@ -16,7 +16,7 @@ import (
 	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/mesh"
-	"github.com/kiali/kiali/mesh/config/cytoscape"
+	"github.com/kiali/kiali/mesh/config/common"
 	"github.com/kiali/kiali/mesh/generator"
 	"github.com/kiali/kiali/observability"
 )
@@ -78,8 +78,8 @@ func generateGraph(meshMap mesh.MeshMap, o mesh.Options) (int, interface{}) {
 
 	var vendorConfig interface{}
 	switch o.ConfigVendor {
-	case graph.VendorCytoscape:
-		vendorConfig = cytoscape.NewConfig(meshMap, o.ConfigOptions)
+	case graph.VendorCommon:
+		vendorConfig = common.NewConfig(meshMap, o.ConfigOptions)
 	default:
 		graph.Error(fmt.Sprintf("ConfigVendor [%s] not supported", o.ConfigVendor))
 	}
diff --git a/mesh/config.go b/mesh/config.go
index 12ecb5cce..4af138cf4 100644
--- a/mesh/config.go
+++ b/mesh/config.go
@@ -5,6 +5,6 @@ type ConfigVendor interface {
 
 	// NewConfig is required by the ConfigVendor interface.  It must produce a valid
 	// Config for the provided TrafficMap, It is recommended to use the graph/util.go
-	// definitions for error handling. Refer to the Cytoscape implementation as an example.
+	// definitions for error handling. Refer to the "Common" implementation as an example.
 	NewConfig(meshMap MeshMap, o ConfigOptions) interface{}
 }
diff --git a/mesh/config/cytoscape/cytoscape.go b/mesh/config/common/common.go
similarity index 85%
rename from mesh/config/cytoscape/cytoscape.go
rename to mesh/config/common/common.go
index e22e0d742..112fbc88d 100644
--- a/mesh/config/cytoscape/cytoscape.go
+++ b/mesh/config/common/common.go
@@ -1,19 +1,14 @@
-// Package cytoscape provides conversion from our graph to the CystoscapeJS
-// configuration json model.
-//
-// The following links are useful for understanding CytoscapeJS and it's configuration:
-//
-// Main page:   http://js.cytoscape.org/
-// JSON config: http://js.cytoscape.org/#notation/elements-json
-// Demos:       http://js.cytoscape.org/#demos
+// Package "common" provides conversion from our graph structure to a common JSON provided back to clients.
+// This is likely the only JSON format that will be needed, although the architecture provides fo multiple providers.
 //
 // Algorithm: Process the graph structure adding nodes and edges, decorating each
-//            with information provided.  An optional second pass generates compound
-//            nodes for requested boxing.
 //
-// The package provides the Cytoscape implementation of graph/ConfigVendor.
+//	with information provided.  An optional second pass generates box
+//	nodes for requested boxing.
+//
+// The package provides the Common implementation of graph/ConfigVendor.
 
-package cytoscape
+package common
 
 import (
 	"crypto/sha256"
@@ -24,18 +19,13 @@ import (
 )
 
 type NodeData struct {
-	// Cytoscape Fields
-	ID     string `json:"id"`               // unique internal node ID (n0, n1...)
-	Parent string `json:"parent,omitempty"` // Compound Node parent ID
-
-	// Required Fields (not required by Cytoscape)
-	Cluster   string `json:"cluster"`
-	InfraName string `json:"infraName"`
-	InfraType string `json:"infraType"`
-	Namespace string `json:"namespace"`
-	NodeType  string `json:"nodeType"`
-
-	// Other Fields
+	ID             string      `json:"id"`               // unique internal node ID (n0, n1...)
+	Parent         string      `json:"parent,omitempty"` // Compound Node parent ID
+	Cluster        string      `json:"cluster"`
+	InfraName      string      `json:"infraName"`
+	InfraType      string      `json:"infraType"`
+	Namespace      string      `json:"namespace"`
+	NodeType       string      `json:"nodeType"`
 	HasInfra       bool        `json:"-"`                        // for local when generating boxes
 	HealthData     interface{} `json:"healthData"`               // data to calculate health status from configurations
 	InfraData      interface{} `json:"infraData,omitempty"`      // infraType-dependent data
@@ -48,12 +38,9 @@ type NodeData struct {
 }
 
 type EdgeData struct {
-	// Cytoscape Fields
 	ID     string `json:"id"`     // unique internal edge ID (e0, e1...)
 	Source string `json:"source"` // parent node ID
 	Target string `json:"target"` // child node ID
-
-	// App Fields (not required by Cytoscape)
 }
 
 type NodeWrapper struct {
diff --git a/mesh/options.go b/mesh/options.go
index ae9b85836..5fe8814f4 100644
--- a/mesh/options.go
+++ b/mesh/options.go
@@ -16,8 +16,8 @@ import (
 
 // The supported vendors
 const (
-	VendorCytoscape     string = "cytoscape"
-	defaultConfigVendor string = VendorCytoscape
+	VendorCommon        string = "common"
+	defaultConfigVendor string = VendorCommon
 )
 
 const (
@@ -97,7 +97,7 @@ func NewOptions(r *http.Request, namespacesService *business.NamespaceService) O
 	}
 	if configVendor == "" {
 		configVendor = defaultConfigVendor
-	} else if configVendor != VendorCytoscape {
+	} else if configVendor != VendorCommon {
 		BadRequest(fmt.Sprintf("Invalid configVendor [%s]", configVendor))
 	}
 	if includeGatewaysString == "" {
diff --git a/routing/routes.go b/routing/routes.go
index a7612fe91..62c12b821 100644
--- a/routing/routes.go
+++ b/routing/routes.go
@@ -680,7 +680,7 @@ func NewRoutes(
 			"ClustersApps",
 			"GET",
 			"/api/clusters/apps",
-			handlers.ClustersApps,
+			handlers.ClusterApps(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
 			true,
 		},
 		// swagger:route GET /namespaces/{namespace}/apps/{app} apps appDetails
@@ -701,7 +701,7 @@ func NewRoutes(
 			"AppDetails",
 			"GET",
 			"/api/namespaces/{namespace}/apps/{app}",
-			handlers.AppDetails,
+			handlers.AppDetails(conf, kialiCache, clientFactory, prom, cpm, traceClientLoader, grafana, discovery),
 			true,
 		},
 		// swagger:route GET /namespaces namespaces namespaceList
@@ -874,9 +874,9 @@ func NewRoutes(
 			handlers.ControlPlaneMetrics(handlers.DefaultPromClientSupplier),
 			true,
 		},
-		// swagger:route GET /namespaces/{namespace}/ztunnel/{workload}/metrics ztunnel metrics
+		// swagger:route GET /namespaces/{namespace}/{workload}/usage_metrics resource usage metrics
 		// ---
-		// Endpoint to fetch metrics to be displayed, related to a single control plane
+		// Endpoint to fetch metrics to be displayed, related to cpu and memory usage
 		//
 		//     Produces:
 		//     - application/json
@@ -889,10 +889,10 @@ func NewRoutes(
 		//      200: metricsResponse
 		//
 		{
-			"ZtunnelMetrics",
+			"UsageMetrics",
 			"GET",
-			"/api/namespaces/{namespace}/ztunnel/{workload}/metrics",
-			handlers.ZtunnelMetrics(handlers.DefaultPromClientSupplier),
+			"/api/namespaces/{namespace}/{app}/usage_metrics",
+			handlers.ResourceUsageMetrics(handlers.DefaultPromClientSupplier),
 			true,
 		},
 		// swagger:route GET /namespaces/{namespace}/services/{service}/dashboard services serviceDashboard
diff --git a/tools/cmd/proxy/main.go b/tools/cmd/proxy/main.go
index b4aac4913..f0e1a29a5 100644
--- a/tools/cmd/proxy/main.go
+++ b/tools/cmd/proxy/main.go
@@ -14,7 +14,7 @@ import (
 
 	"k8s.io/client-go/kubernetes"
 
-	"github.com/kiali/kiali/graph/config/cytoscape"
+	"github.com/kiali/kiali/graph/config/common"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/tools/cmd"
 	"github.com/kiali/kiali/tools/generator"
@@ -56,25 +56,25 @@ func init() {
 	flag.Var(&popStratFlag, "population-strategy", "whether the graph should have many or few connections")
 }
 
-func loadGraphFromFile(filename string) (*cytoscape.Config, error) {
+func loadGraphFromFile(filename string) (*common.Config, error) {
 	contents, err := os.ReadFile(filename)
 	if err != nil {
 		return nil, err
 	}
 
-	cyGraph := &cytoscape.Config{}
-	err = json.Unmarshal(contents, cyGraph)
+	graphConfig := &common.Config{}
+	err = json.Unmarshal(contents, graphConfig)
 	if err != nil {
 		return nil, err
 	}
 
-	return cyGraph, nil
+	return graphConfig, nil
 }
 
 type graphProxy struct {
 	httpProxy *httputil.ReverseProxy
 	generator *generator.Generator
-	graph     *cytoscape.Config
+	graph     *common.Config
 }
 
 func (p graphProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@@ -159,7 +159,7 @@ func main() {
 		log.Fatal(err)
 	}
 
-	var graph *cytoscape.Config
+	var graph *common.Config
 	if dataDirFlag == "" {
 		log.Info("Populating graph data...")
 		g := gen.Generate()
diff --git a/tools/generator/generator.go b/tools/generator/generator.go
index 2cc695ed9..75beadef0 100644
--- a/tools/generator/generator.go
+++ b/tools/generator/generator.go
@@ -16,7 +16,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	"github.com/kiali/kiali/graph"
-	"github.com/kiali/kiali/graph/config/cytoscape"
+	"github.com/kiali/kiali/graph/config/common"
 	"github.com/kiali/kiali/log"
 )
 
@@ -37,7 +37,7 @@ type app struct {
 	IsIngress bool
 }
 
-// Generator creates cytoscape graph data based on the options provided.
+// Generator creates graph data based on the options provided.
 // It is used for testing a variety of graph layouts, large dense graphs in particular,
 // without needing to deploy the actual resources. It is not intended to be used for
 // anything other than testing.
@@ -113,10 +113,10 @@ func New(opts Options) (*Generator, error) {
 // EnsureNamespaces makes sure a kube namespace exists for the nodes.
 // The namespaces need to actually exist in order for the UI to render the graph.
 // Does nothing if a kubeclient is not configured.
-func (g *Generator) EnsureNamespaces(cyGraph cytoscape.Config) error {
+func (g *Generator) EnsureNamespaces(graphConfig common.Config) error {
 	if g.kubeClient != nil {
 		log.Info("Ensuring namespaces exist for graph...")
-		for _, node := range cyGraph.Elements.Nodes {
+		for _, node := range graphConfig.Elements.Nodes {
 			if err := g.ensureNamespace(node.Data.Namespace); err != nil {
 				return err
 			}
@@ -130,7 +130,7 @@ func (g *Generator) EnsureNamespaces(cyGraph cytoscape.Config) error {
 // 1. Workloads send requests to services.
 // 2. Services send requests to the workloads in their app.
 // 3. Ingress workloads are root nodes.
-func (g *Generator) Generate() cytoscape.Config {
+func (g *Generator) Generate() common.Config {
 	nodes := g.generate()
 	traffic := graph.NewTrafficMap()
 	for _, node := range nodes {
@@ -147,13 +147,13 @@ func (g *Generator) Generate() cytoscape.Config {
 		},
 		BoxBy: strings.Join([]string{graph.BoxByApp, graph.BoxByNamespace}, ","),
 	}
-	cyGraph := cytoscape.NewConfig(traffic, opts)
+	graphConfig := common.NewConfig(traffic, opts)
 
-	if err := g.EnsureNamespaces(cyGraph); err != nil {
+	if err := g.EnsureNamespaces(graphConfig); err != nil {
 		log.Errorf("unable to ensure namespaces exist. Err: %s", err)
 	}
 
-	return cyGraph
+	return graphConfig
 }
 
 func (g *Generator) strategyLimit() int {



Test patch:
diff --git a/graph/config/cytoscape/cytoscape_test.go b/graph/config/common/common_test.go
similarity index 89%
rename from graph/config/cytoscape/cytoscape_test.go
rename to graph/config/common/common_test.go
index d125f4d0d..e5be9d506 100644
--- a/graph/config/cytoscape/cytoscape_test.go
+++ b/graph/config/common/common_test.go
@@ -1,4 +1,4 @@
-package cytoscape
+package common
 
 import (
 	"testing"
@@ -92,10 +92,10 @@ func TestHasWorkloadEntryAddedToGraph(t *testing.T) {
 	n0, _ := graph.NewNode("testCluster", "appNamespace", "ratings", "appNamespace", "ratings-v1", "ratings", "v1", graph.GraphTypeVersionedApp)
 	n0.Metadata[graph.HasWorkloadEntry] = []graph.WEInfo{{Name: "ratings-v1"}}
 	traffic[n0.ID] = n0
-	cytoConfig := NewConfig(traffic, graph.ConfigOptions{})
+	graphConfig := NewConfig(traffic, graph.ConfigOptions{})
 
-	cytoNode := cytoConfig.Elements.Nodes[0]
-	assert.Equal(cytoNode.Data.HasWorkloadEntry, n0.Metadata[graph.HasWorkloadEntry])
+	graphNode := graphConfig.Elements.Nodes[0]
+	assert.Equal(graphNode.Data.HasWorkloadEntry, n0.Metadata[graph.HasWorkloadEntry])
 }
 
 func TestHasWorkloadEntryEmpty(t *testing.T) {
@@ -105,10 +105,10 @@ func TestHasWorkloadEntryEmpty(t *testing.T) {
 
 	n0, _ := graph.NewNode("testCluster", "appNamespace", "ratings", "appNamespace", "ratings-v1", "ratings", "v1", graph.GraphTypeVersionedApp)
 	traffic[n0.ID] = n0
-	cytoConfig := NewConfig(traffic, graph.ConfigOptions{})
+	graphConfig := NewConfig(traffic, graph.ConfigOptions{})
 
-	cytoNode := cytoConfig.Elements.Nodes[0]
-	assert.Empty(cytoNode.Data.HasWorkloadEntry)
+	graphNode := graphConfig.Elements.Nodes[0]
+	assert.Empty(graphNode.Data.HasWorkloadEntry)
 }
 
 func TestHTTPToTrafficRate(t *testing.T) {
@@ -135,9 +135,9 @@ func TestHTTPToTrafficRate(t *testing.T) {
 	}
 	e.Metadata[graph.MetadataKey("http")] = 1.00
 
-	cytoConfig := NewConfig(traffic, graph.ConfigOptions{})
+	graphConfig := NewConfig(traffic, graph.ConfigOptions{})
 
-	cytoNode := cytoConfig.Elements.Edges[0]
-	assert.NotNil(cytoNode.Data.Traffic)
-	assert.NotNil(cytoNode.Data.Traffic.Rates)
+	graphNode := graphConfig.Elements.Edges[0]
+	assert.NotNil(graphNode.Data.Traffic)
+	assert.NotNil(graphNode.Data.Traffic.Rates)
 }
diff --git a/handlers/apps_test.go b/handlers/apps_test.go
index 7991bce93..559aaf32e 100644
--- a/handlers/apps_test.go
+++ b/handlers/apps_test.go
@@ -24,12 +24,14 @@ import (
 	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/handlers/authentication"
+	"github.com/kiali/kiali/istio"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/kubernetes/kubetest"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/prometheus"
 	"github.com/kiali/kiali/prometheus/prometheustest"
+	"github.com/kiali/kiali/tracing"
 )
 
 func TestAppMetricsDefault(t *testing.T) {
@@ -198,8 +200,13 @@ func setupAppMetricsEndpoint(t *testing.T) (*httptest.Server, *prometheustest.Pr
 	return ts, xapi, k8s
 }
 
-func setupAppListEndpoint(t *testing.T, k8s kubernetes.ClientInterface, conf config.Config) *httptest.Server {
-	business.SetupBusinessLayer(t, k8s, conf)
+func setupAppListEndpoint(t *testing.T, k8s kubernetes.ClientInterface, conf *config.Config) *httptest.Server {
+	cf := kubetest.NewFakeClientFactoryWithClient(conf, k8s)
+	cache := cache.NewTestingCacheWithFactory(t, cf, *conf)
+	cpm := &business.FakeControlPlaneMonitor{}
+	discovery := istio.NewDiscovery(cf.Clients, cache, conf)
+	traceLoader := func() tracing.ClientInterface { return nil }
+
 	promMock := new(prometheustest.PromAPIMock)
 	promMock.SpyArgumentsAndReturnEmpty(func(mock.Arguments) {})
 	prom, err := prometheus.NewClient()
@@ -207,20 +214,12 @@ func setupAppListEndpoint(t *testing.T, k8s kubernetes.ClientInterface, conf con
 		t.Fatal(err)
 	}
 	prom.Inject(promMock)
-	business.WithProm(prom)
 
+	clusterAppsHandler := WithFakeAuthInfo(conf, ClusterApps(conf, cache, cf, prom, cpm, traceLoader, nil, discovery))
+	appDetailsHandler := WithFakeAuthInfo(conf, AppDetails(conf, cache, cf, prom, cpm, traceLoader, nil, discovery))
 	mr := mux.NewRouter()
-	mr.HandleFunc("/api/clusters/apps", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			context := authentication.SetAuthInfoContext(r.Context(), map[string]*api.AuthInfo{config.Get().KubernetesConfig.ClusterName: {Token: "test"}})
-			ClustersApps(w, r.WithContext(context))
-		}))
-
-	mr.HandleFunc("/api/namespaces/{namespace}/apps/{app}", http.HandlerFunc(
-		func(w http.ResponseWriter, r *http.Request) {
-			context := authentication.SetAuthInfoContext(r.Context(), map[string]*api.AuthInfo{config.Get().KubernetesConfig.ClusterName: {Token: "test"}})
-			AppDetails(w, r.WithContext(context))
-		}))
+	mr.HandleFunc("/api/clusters/apps", clusterAppsHandler)
+	mr.HandleFunc("/api/namespaces/{namespace}/apps/{app}", appDetailsHandler)
 
 	ts := httptest.NewServer(mr)
 	t.Cleanup(ts.Close)
@@ -252,7 +251,7 @@ func TestAppsEndpoint(t *testing.T) {
 	}
 	k8s := kubetest.NewFakeK8sClient(kubeObjects...)
 	k8s.OpenShift = true
-	ts := setupAppListEndpoint(t, k8s, *cfg)
+	ts := setupAppListEndpoint(t, k8s, cfg)
 
 	url := ts.URL + "/api/clusters/apps"
 
@@ -293,7 +292,7 @@ func TestAppDetailsEndpoint(t *testing.T) {
 	}
 	k8s := kubetest.NewFakeK8sClient(kubeObjects...)
 	k8s.OpenShift = true
-	ts := setupAppListEndpoint(t, k8s, *conf)
+	ts := setupAppListEndpoint(t, k8s, conf)
 
 	url := ts.URL + "/api/namespaces/Namespace/apps/httpbin"
 
diff --git a/handlers/testing.go b/handlers/testing.go
index 4c36a3a64..20c1ed99b 100644
--- a/handlers/testing.go
+++ b/handlers/testing.go
@@ -14,6 +14,7 @@ import (
 	core_v1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/tools/clientcmd/api"
 
+	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/handlers/authentication"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/util"
@@ -52,3 +53,9 @@ func WithAuthInfo(authInfo map[string]*api.AuthInfo, hf http.HandlerFunc) http.H
 		hf(w, r.WithContext(context))
 	}
 }
+
+// WithFakeAuthInfo helper for WithAuthInfo that injects a fake token.
+func WithFakeAuthInfo(conf *config.Config, hf http.HandlerFunc) http.HandlerFunc {
+	authInfo := map[string]*api.AuthInfo{conf.KubernetesConfig.ClusterName: {Token: "test"}}
+	return WithAuthInfo(authInfo, hf)
+}
diff --git a/kubernetes/kubetest/mock.go b/kubernetes/kubetest/mock.go
index 14a898e0a..87bc6e4ad 100644
--- a/kubernetes/kubetest/mock.go
+++ b/kubernetes/kubetest/mock.go
@@ -33,7 +33,7 @@ type K8SClientFactoryMock struct {
 var _ kubernetes.ClientFactory = &K8SClientFactoryMock{}
 
 // Constructor
-// Deprecated: use NewFakeClientFactory instead since it doesn't rely on the global config.Get()
+// Deprecated: use NewFakeClientFactory or NewFakeClientFactoryWithClient instead since those doesn't rely on the global config.Get()
 func NewK8SClientFactoryMock(k8s kubernetes.ClientInterface) *K8SClientFactoryMock {
 	conf := config.Get()
 	clients := map[string]kubernetes.ClientInterface{conf.KubernetesConfig.ClusterName: k8s}
@@ -44,6 +44,14 @@ func NewFakeClientFactory(conf *config.Config, clients map[string]kubernetes.Cli
 	return &K8SClientFactoryMock{conf: conf, Clients: clients}
 }
 
+// NewFakeClientFactoryWithClient lets you pass a single client instead of a map.
+func NewFakeClientFactoryWithClient(conf *config.Config, client kubernetes.ClientInterface) *K8SClientFactoryMock {
+	return &K8SClientFactoryMock{
+		conf:    conf,
+		Clients: map[string]kubernetes.ClientInterface{conf.KubernetesConfig.ClusterName: client},
+	}
+}
+
 // Testing specific methods
 func (o *K8SClientFactoryMock) SetClients(clients map[string]kubernetes.ClientInterface) {
 	o.lock.Lock()
diff --git a/tests/integration/multicluster/mesh_test.go b/tests/integration/multicluster/mesh_test.go
index 0cad4ef64..961c3a678 100644
--- a/tests/integration/multicluster/mesh_test.go
+++ b/tests/integration/multicluster/mesh_test.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/kiali/kiali/mesh/config/cytoscape"
+	"github.com/kiali/kiali/mesh/config/common"
 	"github.com/kiali/kiali/tests/integration/utils/kiali"
 	"github.com/kiali/kiali/util/sliceutil"
 )
@@ -18,7 +18,7 @@ func TestMeshShowsExternalControlPlane(t *testing.T) {
 	mesh, err := kiali.MeshGraph()
 	require.NoError(err)
 
-	istiodNodes := sliceutil.Filter(mesh.Elements.Nodes, func(node *cytoscape.NodeWrapper) bool {
+	istiodNodes := sliceutil.Filter(mesh.Elements.Nodes, func(node *common.NodeWrapper) bool {
 		return node.Data.InfraType == "istiod"
 	})
 
diff --git a/tests/integration/utils/kiali/kiali_client.go b/tests/integration/utils/kiali/kiali_client.go
index b5a5da45d..8ff07b8cf 100644
--- a/tests/integration/utils/kiali/kiali_client.go
+++ b/tests/integration/utils/kiali/kiali_client.go
@@ -11,10 +11,10 @@ import (
 
 	"github.com/kiali/kiali/business"
 	"github.com/kiali/kiali/config"
-	"github.com/kiali/kiali/graph/config/cytoscape"
+	"github.com/kiali/kiali/graph/config/common"
 	"github.com/kiali/kiali/handlers"
 	"github.com/kiali/kiali/log"
-	meshcyto "github.com/kiali/kiali/mesh/config/cytoscape"
+	mesh_config_common "github.com/kiali/kiali/mesh/config/common"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/status"
 	"github.com/kiali/kiali/tracing/jaeger/model"
@@ -384,9 +384,9 @@ func IstioPermissions() (*models.IstioConfigPermissions, int, error) {
 	}
 }
 
-func Graph(params map[string]string) (*cytoscape.Config, int, error) {
+func Graph(params map[string]string) (*common.Config, int, error) {
 	url := fmt.Sprintf("%s/api/namespaces/graph?%s", client.kialiURL, ParamsAsString(params))
-	graph := new(cytoscape.Config)
+	graph := new(common.Config)
 
 	code, err := getRequestAndUnmarshalInto(url, graph)
 	if err == nil {
@@ -396,9 +396,9 @@ func Graph(params map[string]string) (*cytoscape.Config, int, error) {
 	}
 }
 
-func ObjectGraph(objectType, graphType, name, namespace string) (*cytoscape.Config, int, error) {
+func ObjectGraph(objectType, graphType, name, namespace string) (*common.Config, int, error) {
 	url := fmt.Sprintf("%s/api/namespaces/%s/%s/%s/graph?duration=60s&graphType=%s", client.kialiURL, namespace, objectType, name, graphType)
-	graph := new(cytoscape.Config)
+	graph := new(common.Config)
 
 	code, err := getRequestAndUnmarshalInto(url, graph)
 	if err == nil {
@@ -408,9 +408,9 @@ func ObjectGraph(objectType, graphType, name, namespace string) (*cytoscape.Conf
 	}
 }
 
-func AppVersionGraph(graphType, name, version, namespace string) (*cytoscape.Config, int, error) {
+func AppVersionGraph(graphType, name, version, namespace string) (*common.Config, int, error) {
 	url := fmt.Sprintf("%s/api/namespaces/%s/applications/%s/versions/%s/graph?duration=60s&graphType=%s", client.kialiURL, namespace, name, version, graphType)
-	graph := new(cytoscape.Config)
+	graph := new(common.Config)
 
 	code, err := getRequestAndUnmarshalInto(url, graph)
 	if err == nil {
@@ -539,9 +539,9 @@ func getRequestAndUnmarshalIntoWithCustomTimeout[T any](url string, timeout time
 	return code, nil
 }
 
-func MeshGraph() (*meshcyto.Config, error) {
+func MeshGraph() (*mesh_config_common.Config, error) {
 	url := fmt.Sprintf("%s/api/mesh/graph", client.kialiURL)
-	meshGraph := new(meshcyto.Config)
+	meshGraph := new(mesh_config_common.Config)
 	_, err := getRequestAndUnmarshalInto(url, meshGraph)
 	if err != nil {
 		return nil, err