From f4d93384138bbe5b09a7dcc8d6e3b0b822ce7106 Mon Sep 17 00:00:00 2001 From: Fernando Hoyos Date: Tue, 5 Nov 2024 12:08:24 +0100 Subject: [PATCH 1/3] Copy of Kiali frontend source code Kiali frontend source originated from: * git ref: master * git commit: 9bc49a5491c47889b7663e00b5c2c9b54eb49e92 * GitHub URL: https://github.com/kiali/kiali/tree/9bc49a5491c47889b7663e00b5c2c9b54eb49e92/frontend/src Signed-off-by: Fernando Hoyos --- .../common/app_details_multicluster-pf.ts | 44 ++- .../kiali/common/graph_display-cy.ts | 18 +- .../kiali/common/graph_display-pf.ts | 42 +- .../integration/kiali/common/graph_replay.ts | 18 +- .../kiali/common/graph_toolbar-cy.ts | 18 +- .../kiali/common/graph_toolbar-pf.ts | 22 +- .../integration/kiali/common/istio_config.ts | 54 +-- .../cypress/integration/kiali/common/mesh.ts | 22 +- .../integration/kiali/common/overview.ts | 21 +- .../kiali/common/service_details.ts | 2 +- .../integration/kiali/common/waypoint.ts | 29 +- .../kiali/common/wizard_request_routing.ts | 21 +- .../app_details_multicluster_graph-pf.feature | 4 +- .../graph_context_menu-pf.feature | 1 + .../featureFiles/graph_display-pf.feature | 1 + .../featureFiles/graph_find_hide-pf.feature | 1 + .../featureFiles/graph_replay-pf.feature | 1 + .../featureFiles/graph_side_panel-pf.feature | 1 + .../featureFiles/graph_toolbar-pf.feature | 1 + .../graph_toolbar_legend-pf.feature | 1 + .../service_details_graph-pf.feature | 2 + .../kiali/featureFiles/waypoint.feature | 39 +- .../workloads_details _graph-pf.feature | 1 + ...oads_details_multicluster_graph-pf.feature | 4 +- plugin/src/kiali/README.md | 6 +- plugin/src/kiali/actions/ActionKeys.ts | 1 + plugin/src/kiali/actions/MeshThunkActions.ts | 5 +- .../IstioConfigCard/IstioConfigCard.tsx | 4 +- .../IstioConfigPreview/IstioConfigPreview.tsx | 4 +- .../components/IstioWizards/ServiceWizard.tsx | 26 +- .../kiali/components/Link/IstioObjectLink.tsx | 4 +- .../kiali/components/Table/ConfigTable.tsx | 2 +- .../kiali/components/VirtualList/Config.ts | 4 +- .../components/VirtualList/Renderers.tsx | 4 +- .../components/VirtualList/VirtualItem.tsx | 4 +- plugin/src/kiali/config/Config.ts | 2 +- plugin/src/kiali/config/ServerConfig.ts | 3 + plugin/src/kiali/helpers/GraphHelpers.tsx | 192 +++++++++ plugin/src/kiali/hooks/services.ts | 13 +- plugin/src/kiali/locales/zh/translation.json | 1 + .../kiali/pages/AppList/FiltersAndSorts.ts | 4 +- .../pages/Graph/GraphToolbar/GraphFindPF.tsx | 112 +++--- .../kiali/pages/Graph/SummaryPanelAppBox.tsx | 2 +- .../pages/Graph/SummaryPanelClusterBox.tsx | 3 +- .../kiali/pages/Graph/SummaryPanelGraph.tsx | 3 +- .../pages/Graph/SummaryPanelNamespaceBox.tsx | 3 +- .../pages/Graph/SummaryPanelNodeTraffic.tsx | 2 +- .../pages/Graph/SummaryPanelTraceDetails.tsx | 2 +- .../kiali/pages/GraphPF/GraphHighlighterPF.ts | 39 +- plugin/src/kiali/pages/GraphPF/GraphPF.tsx | 54 +-- .../src/kiali/pages/GraphPF/GraphPFElems.tsx | 217 +--------- .../src/kiali/pages/GraphPF/GraphPagePF.tsx | 2 +- .../kiali/pages/GraphPF/MiniGraphCardPF.tsx | 16 +- .../src/kiali/pages/GraphPF/NodeDecorator.tsx | 15 +- plugin/src/kiali/pages/GraphPF/TracePF.ts | 12 +- .../components/stylesComponentFactory.tsx | 11 +- .../GraphPF/layouts/ExtendedDagreLayout.ts | 2 +- .../kiali/pages/GraphPF/styles/styleNode.tsx | 13 +- .../IstioConfigOverview.tsx | 9 +- .../IstioConfigList/IstioConfigListPage.tsx | 5 +- .../IstioConfigListComponent.test.ts | 132 +++---- .../IstioConfigNew/IstioConfigNewPage.tsx | 60 +-- plugin/src/kiali/pages/Mesh/Mesh.tsx | 22 +- plugin/src/kiali/pages/Mesh/MeshElems.tsx | 193 +-------- .../src/kiali/pages/Mesh/MeshHighlighter.ts | 47 ++- .../Mesh/components/meshComponentFactory.tsx | 42 +- .../src/kiali/pages/Mesh/styles/MeshEdge.tsx | 13 +- .../src/kiali/pages/Mesh/styles/MeshNode.tsx | 23 +- .../kiali/pages/Mesh/target/TargetPanel.tsx | 48 +-- .../pages/Mesh/target/TargetPanelCluster.tsx | 28 +- .../pages/Mesh/target/TargetPanelCommon.tsx | 4 +- .../Mesh/target/TargetPanelConfigTable.tsx | 2 +- .../Mesh/target/TargetPanelControlPlane.tsx | 102 ++--- .../Mesh/target/TargetPanelDataPlane.tsx | 17 +- .../target/TargetPanelDataPlaneNamespace.tsx | 4 +- .../pages/Mesh/target/TargetPanelMesh.tsx | 12 +- .../Mesh/target/TargetPanelNamespace.tsx | 372 +++++------------- .../pages/Mesh/target/TargetPanelNode.tsx | 15 +- .../src/kiali/pages/Mesh/toolbar/MeshFind.tsx | 88 +++-- .../pages/Overview/CanaryUpgradeProgress.tsx | 53 --- .../pages/Overview/ControlPlaneDonut.tsx | 103 +++++ .../src/kiali/pages/Overview/OverviewPage.tsx | 107 ++--- .../Overview/OverviewTrafficPolicies.tsx | 41 +- .../ServiceDetails/ServiceDetailsPage.tsx | 20 +- .../pages/ServiceList/FiltersAndSorts.ts | 4 +- .../pages/WorkloadDetails/WorkloadInfo.tsx | 51 +-- .../pages/WorkloadList/FiltersAndSorts.ts | 4 +- .../__tests__/NamespacesReducer.test.ts | 5 +- plugin/src/kiali/services/Api.ts | 7 +- plugin/src/kiali/store/Store.ts | 11 +- plugin/src/kiali/styles/GlobalStyle.ts | 2 +- plugin/src/kiali/styles/variables.module.scss | 5 - plugin/src/kiali/types/Health.ts | 2 +- plugin/src/kiali/types/IstioConfigDetails.ts | 46 +-- plugin/src/kiali/types/IstioConfigList.ts | 248 +++--------- plugin/src/kiali/types/IstioObjects.ts | 8 +- plugin/src/kiali/types/Mesh.ts | 139 ++++++- plugin/src/kiali/types/Namespace.ts | 12 +- plugin/src/kiali/types/NamespaceInfo.ts | 1 + plugin/src/kiali/utils/IstioConfigUtils.ts | 52 +-- .../src/kiali/utils/IstioValidationUtils.ts | 6 +- .../utils/__tests__/IstioConfigUtils.test.ts | 12 +- 102 files changed, 1394 insertions(+), 1938 deletions(-) create mode 100644 plugin/src/kiali/helpers/GraphHelpers.tsx delete mode 100644 plugin/src/kiali/pages/Overview/CanaryUpgradeProgress.tsx create mode 100644 plugin/src/kiali/pages/Overview/ControlPlaneDonut.tsx diff --git a/plugin/cypress/integration/kiali/common/app_details_multicluster-pf.ts b/plugin/cypress/integration/kiali/common/app_details_multicluster-pf.ts index 19a4a24c..9305b6da 100644 --- a/plugin/cypress/integration/kiali/common/app_details_multicluster-pf.ts +++ b/plugin/cypress/integration/kiali/common/app_details_multicluster-pf.ts @@ -4,6 +4,7 @@ import { clusterParameterExists } from './navigation'; import { ensureKialiFinishedLoading } from './transition'; import { elems, nodeInfo } from './graph-pf'; import { Visualization } from '@patternfly/react-topology'; +import { NodeType } from 'types/Graph'; Then('user sees details information for the remote {string} app', (name: string) => { cy.getBySel('app-description-card').within(() => { @@ -100,13 +101,26 @@ Given( assert.isTrue(controller.hasGraph()); const { nodes } = elems(controller); - const nodeExists = nodes.some( - node => + const nodeExists = nodes.some(node => { + const nodeOk = node.getData().nodeType === nodeType && node.getData().namespace === 'bookinfo' && node.getData().cluster === cluster && - node.getData().isBox === isBox - ); + node.getData().isBox === isBox; + if (!nodeOk) { + return false; + } + switch (type) { + case NodeType.APP: + return node.getData().app === name; + case NodeType.SERVICE: + return node.getData().service === name; + case NodeType.WORKLOAD: + return node.getData().workload === name; + default: + return false; + } + }); assert(nodeExists, `Node ${name} of type ${type} from cluster ${cluster} not found in the graph`); }); @@ -129,14 +143,26 @@ When( assert.isTrue(controller.hasGraph()); const { nodes } = elems(controller); - const node = nodes.find( - node => + const node = nodes.find(node => { + const nodeOk = node.getData().nodeType === nodeType && node.getData().namespace === 'bookinfo' && node.getData().cluster === cluster && - node.getData().isBox === isBox && - !node.getData().isInaccessible - ); + node.getData().isBox === isBox; + if (!nodeOk) { + return false; + } + switch (type) { + case NodeType.APP: + return node.getData().app === name; + case NodeType.SERVICE: + return node.getData().service === name; + case NodeType.WORKLOAD: + return node.getData().workload === name; + default: + return false; + } + }); assert(node, `Node ${name} of type ${type} from cluster ${cluster} not found in the graph`); diff --git a/plugin/cypress/integration/kiali/common/graph_display-cy.ts b/plugin/cypress/integration/kiali/common/graph_display-cy.ts index 2c4aef40..5718e59a 100644 --- a/plugin/cypress/integration/kiali/common/graph_display-cy.ts +++ b/plugin/cypress/integration/kiali/common/graph_display-cy.ts @@ -1,22 +1,6 @@ -import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; +import { Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { ensureKialiFinishedLoading } from './transition'; -Before(() => { - // Copied from overview.ts. This prevents cypress from stopping on errors unrelated to the tests. - // There can be random failures due timeouts/loadtime/framework that throw browser errors. This - // prevents a CI failure due something like a "slow". There may be a better way to handle this. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); - 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'); diff --git a/plugin/cypress/integration/kiali/common/graph_display-pf.ts b/plugin/cypress/integration/kiali/common/graph_display-pf.ts index 81575da9..70363dda 100644 --- a/plugin/cypress/integration/kiali/common/graph_display-pf.ts +++ b/plugin/cypress/integration/kiali/common/graph_display-pf.ts @@ -1,25 +1,9 @@ -import { Before, Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'; +import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { ensureKialiFinishedLoading } from './transition'; import { Visualization } from '@patternfly/react-topology'; import { elems, select, selectAnd, selectOr } from './graph-pf'; import { EdgeAttr, NodeAttr } from 'types/Graph'; -Before(() => { - // Copied from overview.ts. This prevents cypress from stopping on errors unrelated to the tests. - // There can be random failures due timeouts/loadtime/framework that throw browser errors. This - // prevents a CI failure due something like a "slow". There may be a better way to handle this. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); - When('user graphs {string} namespaces', (namespaces: string) => { // Forcing "Pause" to not cause unhandled promises from the browser when cypress is testing cy.intercept(`**/api/namespaces/graph*`).as('graphNamespaces'); @@ -449,10 +433,9 @@ Given( .as(`istioConfigRequest-${cluster}`) .then(response => { expect(response.status).to.eq(200); - expect(response.body).to.have.property('gateways'); - expect(response.body).to.have.property('virtualServices'); - expect(response.body.gateways).to.have.length.gte(1); - expect(response.body.virtualServices).to.have.length.gte(1); + expect(response.body).to.have.property('resources'); + expect(response.body.resources['networking.istio.io/v1, Kind=Gateway'].length).greaterThan(0); + expect(response.body.resources['networking.istio.io/v1, Kind=VirtualService'].length).greaterThan(0); }); } ); @@ -470,12 +453,15 @@ Then( }); cy.get('@istioConfigRequest-east').then(resp => { - // Not going to check all the objects. Just the ones that probably exist while testing. - const totalObjectsEast = - resp.body.gateways.length + resp.body.virtualServices.length + resp.body.destinationRules.length; + let totalObjectsEast = 0; + Object.keys(resp.body.resources).forEach(resourceKey => { + totalObjectsEast += resp.body.resources[resourceKey].length; + }); cy.get('@istioConfigRequest-west').then(resp => { - const totalObjectsWest = - resp.body.gateways.length + resp.body.virtualServices.length + resp.body.destinationRules.length; + let totalObjectsWest = 0; + Object.keys(resp.body.resources).forEach(resourceKey => { + totalObjectsEast += resp.body.resources[resourceKey].length; + }); const totalObjects = totalObjectsEast + totalObjectsWest; cy.get('[aria-label="Validations list"]').contains(`Istio config objects analyzed: ${totalObjects}`); }); @@ -497,6 +483,8 @@ When('user {string} {string} traffic option', (action: string, option: string) = Then('{int} edges appear in the graph', (graphEdges: number) => { cy.waitForReact(); + ensureKialiFinishedLoading(); + cy.getReact('GraphPagePFComponent', { state: { isReady: true } }) .should('have.length', '1') .then($graph => { @@ -526,7 +514,7 @@ Then('the {string} node {string} exists', (nodeName: string, action: string) => const foundNode = nodes.filter(node => node.getData().workload === nodeName); if (action === 'does') { - assert.isAtLeast(foundNode.length, 1); + assert.equal(foundNode.length, 1); } else { assert.equal(foundNode.length, 0); } diff --git a/plugin/cypress/integration/kiali/common/graph_replay.ts b/plugin/cypress/integration/kiali/common/graph_replay.ts index f9cc1a09..3b1356de 100644 --- a/plugin/cypress/integration/kiali/common/graph_replay.ts +++ b/plugin/cypress/integration/kiali/common/graph_replay.ts @@ -1,20 +1,4 @@ -import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; - -Before(() => { - // Copied from overview.ts. This prevents cypress from stopping on errors unrelated to the tests. - // There can be random failures due timeouts/loadtime/framework that throw browser errors. This - // prevents a CI failure due something like a "slow". There may be a better way to handle this. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); +import { Then, When } from '@badeball/cypress-cucumber-preprocessor'; When('user presses the Replay button', () => { cy.get('button[data-test="graph-replay-button"]').click(); diff --git a/plugin/cypress/integration/kiali/common/graph_toolbar-cy.ts b/plugin/cypress/integration/kiali/common/graph_toolbar-cy.ts index 612eb3ae..fd454b19 100644 --- a/plugin/cypress/integration/kiali/common/graph_toolbar-cy.ts +++ b/plugin/cypress/integration/kiali/common/graph_toolbar-cy.ts @@ -1,22 +1,6 @@ -import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; +import { Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { CytoscapeGlobalScratchData, CytoscapeGlobalScratchNamespace } from 'types/Graph'; -Before(() => { - // Copied from overview.ts. This prevents cypress from stopping on errors unrelated to the tests. - // There can be random failures due timeouts/loadtime/framework that throw browser errors. This - // prevents a CI failure due something like a "slow". There may be a better way to handle this. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); - When( 'user graphs {string} namespaces with refresh {string} and duration {string} in the cytoscape graph', (namespaces: string, refresh: string, duration: string) => { diff --git a/plugin/cypress/integration/kiali/common/graph_toolbar-pf.ts b/plugin/cypress/integration/kiali/common/graph_toolbar-pf.ts index 72ded936..35d2f7dd 100644 --- a/plugin/cypress/integration/kiali/common/graph_toolbar-pf.ts +++ b/plugin/cypress/integration/kiali/common/graph_toolbar-pf.ts @@ -1,24 +1,8 @@ -import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; +import { Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { EdgeAttr } from 'types/Graph'; import { elems, select } from './graph-pf'; import { Visualization } from '@patternfly/react-topology'; -Before(() => { - // Copied from overview.ts. This prevents cypress from stopping on errors unrelated to the tests. - // There can be random failures due timeouts/loadtime/framework that throw browser errors. This - // prevents a CI failure due something like a "slow". There may be a better way to handle this. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); - When( 'user graphs {string} namespaces with refresh {string} and duration {string}', (namespaces: string, refresh: string, duration: string) => { @@ -88,9 +72,9 @@ When('user selects {string} graph type', (graphType: string) => { Then('user {string} graph tour', (action: string) => { if (action === 'sees') { - cy.get('div[role="dialog"]').find('span').contains('Shortcuts').should('exist'); + cy.get('.pf-v5-c-popover').find('span').contains('Shortcuts').should('exist'); } else { - cy.get('div[role="dialog"]').should('not.exist'); + cy.get('.pf-v5-c-popover').should('not.exist'); } }); diff --git a/plugin/cypress/integration/kiali/common/istio_config.ts b/plugin/cypress/integration/kiali/common/istio_config.ts index f23bfc3e..17d6169d 100644 --- a/plugin/cypress/integration/kiali/common/istio_config.ts +++ b/plugin/cypress/integration/kiali/common/istio_config.ts @@ -1,6 +1,7 @@ import { After, Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { colExists, getColWithRowText } from './table'; import { ensureKialiFinishedLoading } from './transition'; +import { getGVKTypeString } from 'utils/IstioConfigUtils'; const CLUSTER1_CONTEXT = Cypress.env('CLUSTER1_CONTEXT'); const CLUSTER2_CONTEXT = Cypress.env('CLUSTER2_CONTEXT'); @@ -24,54 +25,6 @@ const labelsStringToJson = (labelsString: string): string => { return `{${labelsJson}}`; }; -// This is for Istio Object Types only -const pluralize = (word: string): string => { - const endings = { - ay: 'ays', - cy: 'cies', - ry: 'ries', - ze: 'zes', - s: 'ses', - e: 'es' - }; - - for (const [singular, plural] of Object.entries(endings)) { - const regex = new RegExp(`${singular}$`); - if (regex.test(word)) { - return word.replace(regex, plural); - } - } - - // 's' by default - return `${word}s`; -}; - -export const dicIstioTypeToGVKStrings: { [key: string]: string } = { - AuthorizationPolicy: 'security.istio.io/v1, Kind=AuthorizationPolicy', - PeerAuthentication: 'security.istio.io/v1, Kind=PeerAuthentication', - RequestAuthentication: 'security.istio.io/v1, Kind=RequestAuthentication', - - DestinationRule: 'networking.istio.io/v1, Kind=DestinationRule', - Gateway: 'networking.istio.io/v1, Kind=Gateway', - EnvoyFilter: 'networking.istio.io/v1alpha3, Kind=EnvoyFilter', - Sidecar: 'networking.istio.io/v1, Kind=Sidecar', - ServiceEntry: 'networking.istio.io/v1, Kind=ServiceEntry', - VirtualService: 'networking.istio.io/v1, Kind=VirtualService', - WorkloadEntry: 'networking.istio.io/v1, Kind=WorkloadEntry', - WorkloadGroup: 'networking.istio.io/v1, Kind=WorkloadGroup', - - WasmPlugin: 'extensions.istio.io/v1alpha1, Kind=WasmPlugin', - Telemetry: 'telemetry.istio.io/v1, Kind=Telemetry', - - K8sGateway: 'gateway.networking.k8s.io/v1, Kind=Gateway', - K8sGatewayClass: 'gateway.networking.k8s.io/v1, Kind=GatewayClass', - K8sGRPCRoute: 'gateway.networking.k8s.io/v1, Kind=GRPCRoute', - K8sHTTPRoute: 'gateway.networking.k8s.io/v1, Kind=HTTPRoute', - K8sReferenceGrant: 'gateway.networking.k8s.io/v1, Kind=ReferenceGrant', - K8sTCPRoute: 'gateway.networking.k8s.io/v1alpha2, Kind=TCPRoute', - K8sTLSRoute: 'gateway.networking.k8s.io/v1alpha2, Kind=TLSRoute' -}; - const minimalAuthorizationPolicy = (name: string, namespace: string): string => { return `{ "apiVersion": "security.istio.io/v1", @@ -590,14 +543,13 @@ Then('user only sees {string}', (sees: string) => { }); Then('only {string} objects are visible in the {string} namespace', (sees: string, ns: string) => { - let lowercaseSees: string = sees.charAt(0).toLowerCase() + sees.slice(1); let count: number; cy.request({ method: 'GET', - url: `/api/namespaces/${ns}/istio?objects=${dicIstioTypeToGVKStrings[sees]}&validate=true` + url: `/api/namespaces/${ns}/istio?objects=${getGVKTypeString(sees)}&validate=true` }).then(response => { - count = response.body[pluralize(lowercaseSees)].length; + count = response.body['resources'][getGVKTypeString(sees)].length; }); cy.get('tbody').contains('tr', sees); diff --git a/plugin/cypress/integration/kiali/common/mesh.ts b/plugin/cypress/integration/kiali/common/mesh.ts index 529970b4..9d7b7a8d 100644 --- a/plugin/cypress/integration/kiali/common/mesh.ts +++ b/plugin/cypress/integration/kiali/common/mesh.ts @@ -1,24 +1,8 @@ -import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; +import { Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { Visualization } from '@patternfly/react-topology'; import { MeshInfraType, MeshNodeData } from 'types/Mesh'; import { elems } from './graph-pf'; -Before(() => { - // Copied from overview.ts. This prevents cypress from stopping on errors unrelated to the tests. - // There can be random failures due timeouts/loadtime/framework that throw browser errors. This - // prevents a CI failure due something like a "slow". There may be a better way to handle this. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); - When('user closes mesh tour', () => { cy.waitForReact(); cy.get('div[role="dialog"]').find('button[aria-label="Close"]').click(); @@ -189,9 +173,9 @@ Then('user sees the istiod node connected to the dataplane nodes', () => { Then('user {string} mesh tour', (action: string) => { cy.waitForReact(); if (action === 'sees') { - cy.get('div[class*="pf-v5-c-popover"]').find('span').contains('Shortcuts').should('exist'); + cy.get('.pf-v5-c-popover').find('span').contains('Shortcuts').should('exist'); } else { - cy.get('div[class*="pf-v5-c-popover"]').should('not.exist'); + cy.get('.pf-v5-c-popover').should('not.exist'); } }); diff --git a/plugin/cypress/integration/kiali/common/overview.ts b/plugin/cypress/integration/kiali/common/overview.ts index d09472fa..1c5869ae 100644 --- a/plugin/cypress/integration/kiali/common/overview.ts +++ b/plugin/cypress/integration/kiali/common/overview.ts @@ -1,29 +1,10 @@ -import { Before, Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'; +import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { ensureKialiFinishedLoading } from './transition'; import { getClusterForSingleCluster } from './table'; const CLUSTER1_CONTEXT = Cypress.env('CLUSTER1_CONTEXT'); const CLUSTER2_CONTEXT = Cypress.env('CLUSTER2_CONTEXT'); -Before(() => { - // Focing to not stop cypress on unexpected errors not related to the tests. - // There are some random failures due timeouts/loadtime/framework that throws some error in the browser. - // After reviewing the tests failures, those are unrelated to the app, so, - // it needs this event to not fail the CI action due some "slow" action or similar. - // This is something to review in future iterations when tests are solid, but I haven't found a better way to - // solve this issue. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); - Given('a healthy application in the cluster', function () { this.targetNamespace = 'bookinfo'; this.targetApp = 'productpage'; diff --git a/plugin/cypress/integration/kiali/common/service_details.ts b/plugin/cypress/integration/kiali/common/service_details.ts index 46604521..6f0c56a7 100644 --- a/plugin/cypress/integration/kiali/common/service_details.ts +++ b/plugin/cypress/integration/kiali/common/service_details.ts @@ -84,7 +84,7 @@ Then('sd::user does not see No data message in the {string} graph', (graph: stri When('user chooses the {string} option', (title: string) => { cy.wait('@waitForCall'); - cy.get('button[aria-label="Actions"]').click(); + cy.get('#minigraph-toggle').click(); cy.contains(title).should('be.visible'); cy.contains(title).click(); diff --git a/plugin/cypress/integration/kiali/common/waypoint.ts b/plugin/cypress/integration/kiali/common/waypoint.ts index e3a260af..b64ae051 100644 --- a/plugin/cypress/integration/kiali/common/waypoint.ts +++ b/plugin/cypress/integration/kiali/common/waypoint.ts @@ -30,7 +30,30 @@ const waitForWorkloadEnrolled = (maxRetries = 30, retryCount = 0): void => { return; } else { return cy.wait(10000).then(() => { - return waitForWorkloadEnrolled(maxRetries, retryCount + 1); // Ensure to return the recursive call + return waitForWorkloadEnrolled(maxRetries, retryCount + 1); + }); + } + }); +}; + +const waitForBookinfoWaypointTrafficGeneratedInGraph = (maxRetries = 30, retryCount = 0): void => { + if (retryCount >= maxRetries) { + throw new Error(`Condition not met after ${maxRetries} retries`); + } + + cy.request({ + method: 'GET', + url: + '/api/namespaces/graph?duration=60s&graphType=versionedApp&includeIdleEdges=false&injectServiceNodes=true&boxBy=cluster,namespace,app&waypoints=false&ambientTraffic=waypoint&appenders=deadNode,istio,serviceEntry,meshCheck,workloadEntry,health,ambient&rateGrpc=requests&rateHttp=requests&rateTcp=sent&namespaces=bookinfo' + }).then(response => { + expect(response.status).to.equal(200); + const elements = response.body.elements; + + if (elements?.edges?.length > 10) { + return; + } else { + return cy.wait(10000).then(() => { + return waitForBookinfoWaypointTrafficGeneratedInGraph(maxRetries, retryCount + 1); }); } }); @@ -41,6 +64,10 @@ Then('{string} namespace is labeled with the waypoint label', (namespace: string waitForWorkloadEnrolled(); }); +Then('the graph page has enough data', () => { + waitForBookinfoWaypointTrafficGeneratedInGraph(); +}); + Then('the user hovers in the {string} label and sees {string} in the tooltip', (label: string, text: string) => { cy.get(`[data-test=workload-description-card]`).contains('span', label).trigger('mouseenter'); cy.get('[role="tooltip"]').should('be.visible').and('contain', text); diff --git a/plugin/cypress/integration/kiali/common/wizard_request_routing.ts b/plugin/cypress/integration/kiali/common/wizard_request_routing.ts index e95b66da..77e9d9d2 100644 --- a/plugin/cypress/integration/kiali/common/wizard_request_routing.ts +++ b/plugin/cypress/integration/kiali/common/wizard_request_routing.ts @@ -1,28 +1,9 @@ -import { Before, Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; +import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'; import { ensureKialiFinishedLoading } from './transition'; const CLUSTER1_CONTEXT = Cypress.env('CLUSTER1_CONTEXT'); const CLUSTER2_CONTEXT = Cypress.env('CLUSTER2_CONTEXT'); -Before(() => { - // Forcing to not stop cypress on unexpected errors not related to the tests. - // There are some random failures due timeouts/loadtime/framework that throws some error in the browser. - // After reviewing the tests failures, those are unrelated to the app, so, - // it needs this event to not fail the CI action due some "slow" action or similar. - // This is something to review in future iterations when tests are solid, but I haven't found a better way to - // solve this issue. - cy.on('uncaught:exception', (err, runnable, promise) => { - // when the exception originated from an unhandled promise - // rejection, the promise is provided as a third argument - // you can turn off failing the test in this case - if (promise) { - return false; - } - // we still want to ensure there are no other unexpected - // errors, so we let them fail the test - }); -}); - Given('user opens the namespace {string} and {string} service details page', (namespace: string, service: string) => { // Forcing "Pause" to not cause unhandled promises from the browser when cypress is testing cy.visit({ url: `/console/namespaces/${namespace}/services/${service}?refresh=0` }); diff --git a/plugin/cypress/integration/kiali/featureFiles/app_details_multicluster_graph-pf.feature b/plugin/cypress/integration/kiali/featureFiles/app_details_multicluster_graph-pf.feature index 8b646005..5dcd5be2 100644 --- a/plugin/cypress/integration/kiali/featureFiles/app_details_multicluster_graph-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/app_details_multicluster_graph-pf.feature @@ -28,9 +28,7 @@ Feature: Kiali App Details page minigraph in multicluster setup | type | name | | app | reviews | | service | reviews | - - # TODO Fix issue https://github.com/kiali/kiali/issues/7839 - # | workload | reviews-v3 | + | 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. diff --git a/plugin/cypress/integration/kiali/featureFiles/graph_context_menu-pf.feature b/plugin/cypress/integration/kiali/featureFiles/graph_context_menu-pf.feature index a16af2f7..ecf827cc 100644 --- a/plugin/cypress/integration/kiali/featureFiles/graph_context_menu-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/graph_context_menu-pf.feature @@ -1,4 +1,5 @@ @graph-context-menu +@ossmc # 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 diff --git a/plugin/cypress/integration/kiali/featureFiles/graph_display-pf.feature b/plugin/cypress/integration/kiali/featureFiles/graph_display-pf.feature index 9d3a4d1d..8bee5be6 100644 --- a/plugin/cypress/integration/kiali/featureFiles/graph_display-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/graph_display-pf.feature @@ -1,4 +1,5 @@ @graph-display +@ossmc # 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 diff --git a/plugin/cypress/integration/kiali/featureFiles/graph_find_hide-pf.feature b/plugin/cypress/integration/kiali/featureFiles/graph_find_hide-pf.feature index 8dcf1559..091952d4 100644 --- a/plugin/cypress/integration/kiali/featureFiles/graph_find_hide-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/graph_find_hide-pf.feature @@ -1,4 +1,5 @@ @graph-page-find-hide +@ossmc # 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 diff --git a/plugin/cypress/integration/kiali/featureFiles/graph_replay-pf.feature b/plugin/cypress/integration/kiali/featureFiles/graph_replay-pf.feature index a17e1d5d..40e61a1e 100644 --- a/plugin/cypress/integration/kiali/featureFiles/graph_replay-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/graph_replay-pf.feature @@ -1,4 +1,5 @@ @graph-page-replay +@ossmc # 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 diff --git a/plugin/cypress/integration/kiali/featureFiles/graph_side_panel-pf.feature b/plugin/cypress/integration/kiali/featureFiles/graph_side_panel-pf.feature index b8e5ed95..7a692dd1 100644 --- a/plugin/cypress/integration/kiali/featureFiles/graph_side_panel-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/graph_side_panel-pf.feature @@ -1,4 +1,5 @@ @graph-side-panel +@ossmc # 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 diff --git a/plugin/cypress/integration/kiali/featureFiles/graph_toolbar-pf.feature b/plugin/cypress/integration/kiali/featureFiles/graph_toolbar-pf.feature index 1bd9f296..9fe55935 100644 --- a/plugin/cypress/integration/kiali/featureFiles/graph_toolbar-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/graph_toolbar-pf.feature @@ -1,4 +1,5 @@ @graph-toolbar +@ossmc # 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) diff --git a/plugin/cypress/integration/kiali/featureFiles/graph_toolbar_legend-pf.feature b/plugin/cypress/integration/kiali/featureFiles/graph_toolbar_legend-pf.feature index a7efd959..e3f4781f 100644 --- a/plugin/cypress/integration/kiali/featureFiles/graph_toolbar_legend-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/graph_toolbar_legend-pf.feature @@ -1,4 +1,5 @@ @graph-toolbar-legend +@ossmc # 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 diff --git a/plugin/cypress/integration/kiali/featureFiles/service_details_graph-pf.feature b/plugin/cypress/integration/kiali/featureFiles/service_details_graph-pf.feature index a546a81c..20bdffa8 100644 --- a/plugin/cypress/integration/kiali/featureFiles/service_details_graph-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/service_details_graph-pf.feature @@ -1,4 +1,5 @@ @service-details +@ossmc # 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 @@ -15,6 +16,7 @@ Feature: Kiali Service Details page Then user sees a minigraph @bookinfo-app + @skip-ossmc Scenario: Verify that the Graph type dropdown is disabled when changing to Show node graph When user sees a minigraph And user chooses the "Show node graph" option diff --git a/plugin/cypress/integration/kiali/featureFiles/waypoint.feature b/plugin/cypress/integration/kiali/featureFiles/waypoint.feature index 0e2d5f78..22b2d955 100644 --- a/plugin/cypress/integration/kiali/featureFiles/waypoint.feature +++ b/plugin/cypress/integration/kiali/featureFiles/waypoint.feature @@ -13,6 +13,7 @@ Feature: Kiali Waypoint related features @waypoint Scenario: namespace is labeled with waypoint label Then "bookinfo" namespace is labeled with the waypoint label + And the graph page has enough data @waypoint Scenario: See the waypoint workload with the correct info @@ -43,16 +44,6 @@ Feature: Kiali Waypoint related features And user "enables" "ambientZtunnel" traffic option Then 7 edges appear in the graph - # TODO Fix waypoint issue - #@waypoint - #Scenario: User sees waypoint traffic - # Given user is at the "graphpf" page - # When user graphs "bookinfo" namespaces - # Then user sees the "bookinfo" namespace - # Then user opens traffic menu - # And user "enables" "ambientWaypoint" traffic option - # Then 11 edges appear in the graph - @waypoint Scenario: User sees no Ambient traffic Given user is at the "graphpf" page @@ -73,17 +64,27 @@ Feature: Kiali Waypoint related features Then 16 edges appear in the graph @waypoint - Scenario: User sees doesn't see waypoint proxy + Scenario: User doesn't see waypoint proxy And the "waypoint" node "doesn't" exists - # TODO Fix waypoint issue - #@waypoint - #Scenario: User sees waypoint proxy - # When user opens display menu - # Then the display menu opens - # Then user "enables" "filterWaypoints" edge labels - # Then 16 edges appear in the graph - # And the "waypoint" node "does" exists + @waypoint + Scenario: User sees waypoint proxy + When user opens display menu + Then the display menu opens + Then user "enables" "filterWaypoints" edge labels + Then user opens traffic menu + And user "enables" "ambientTotal" traffic option + Then 16 edges appear in the graph + And the "waypoint" node "does" exists + + @waypoint + Scenario: User sees waypoint traffic + Given user is at the "graphpf" page + When user graphs "bookinfo" namespaces + Then user sees the "bookinfo" namespace + Then user opens traffic menu + And user "enables" "ambientWaypoint" traffic option + Then 11 edges appear in the graph @waypoint Scenario: Waypoint should not have validation errors diff --git a/plugin/cypress/integration/kiali/featureFiles/workloads_details _graph-pf.feature b/plugin/cypress/integration/kiali/featureFiles/workloads_details _graph-pf.feature index 5543e5ec..c58e958b 100644 --- a/plugin/cypress/integration/kiali/featureFiles/workloads_details _graph-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/workloads_details _graph-pf.feature @@ -1,4 +1,5 @@ @workload-details +@ossmc # 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 diff --git a/plugin/cypress/integration/kiali/featureFiles/workloads_details_multicluster_graph-pf.feature b/plugin/cypress/integration/kiali/featureFiles/workloads_details_multicluster_graph-pf.feature index 94ddf6bd..32338a9e 100644 --- a/plugin/cypress/integration/kiali/featureFiles/workloads_details_multicluster_graph-pf.feature +++ b/plugin/cypress/integration/kiali/featureFiles/workloads_details_multicluster_graph-pf.feature @@ -25,9 +25,7 @@ Feature: Kiali Workloads Details minigraph in multicluster setup Examples: | type | name | | service | reviews | - - # TODO Fix issue https://github.com/kiali/kiali/issues/7839 - # | workload | reviews-v3 | + | 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. diff --git a/plugin/src/kiali/README.md b/plugin/src/kiali/README.md index dfbacc31..4c859d88 100644 --- a/plugin/src/kiali/README.md +++ b/plugin/src/kiali/README.md @@ -2,6 +2,6 @@ Copy of Kiali frontend source code Kiali frontend source originated from: -* git ref: v2.0 -* git commit: 1cb221e8ce373ec3a46ffc136258013c97387f21 -* GitHub URL: https://github.com/kiali/kiali/tree/1cb221e8ce373ec3a46ffc136258013c97387f21/frontend/src +* git ref: master +* git commit: 9bc49a5491c47889b7663e00b5c2c9b54eb49e92 +* GitHub URL: https://github.com/kiali/kiali/tree/9bc49a5491c47889b7663e00b5c2c9b54eb49e92/frontend/src diff --git a/plugin/src/kiali/actions/ActionKeys.ts b/plugin/src/kiali/actions/ActionKeys.ts index cfcc03cc..568b8ea2 100644 --- a/plugin/src/kiali/actions/ActionKeys.ts +++ b/plugin/src/kiali/actions/ActionKeys.ts @@ -41,6 +41,7 @@ export enum ActionKeys { GRAPH_UPDATE_SUMMARY = 'GRAPH_UPDATE_SUMMARY', + MESH_SET_CONTROLPLANES = 'MESH_SET_CONTROLPLANES', MESH_SET_DEFINITION = 'MESH_SET_DEFINITION', MESH_SET_LAYOUT = 'MESH_SET_LAYOUT', MESH_SET_TARGET = 'MESH_SET_TARGET', diff --git a/plugin/src/kiali/actions/MeshThunkActions.ts b/plugin/src/kiali/actions/MeshThunkActions.ts index 55fa4402..235d0665 100644 --- a/plugin/src/kiali/actions/MeshThunkActions.ts +++ b/plugin/src/kiali/actions/MeshThunkActions.ts @@ -1,14 +1,15 @@ +import { MeshType } from 'types/Mesh'; import { KialiDispatch } from '../types/Redux'; import { MeshActions } from './MeshActions'; import { Controller } from '@patternfly/react-topology'; export const MeshThunkActions = { meshReady: (controller: Controller) => { - return (dispatch: KialiDispatch) => { + return (dispatch: KialiDispatch): void => { dispatch( MeshActions.setTarget({ elem: controller, - type: 'mesh' + type: MeshType.Mesh }) ); }; diff --git a/plugin/src/kiali/components/IstioConfigCard/IstioConfigCard.tsx b/plugin/src/kiali/components/IstioConfigCard/IstioConfigCard.tsx index be8765c9..28356d57 100644 --- a/plugin/src/kiali/components/IstioConfigCard/IstioConfigCard.tsx +++ b/plugin/src/kiali/components/IstioConfigCard/IstioConfigCard.tsx @@ -18,7 +18,7 @@ import { kialiStyle } from 'styles/StyleUtils'; import { PFBadge } from '../Pf/PfBadges'; import { IstioObjectLink } from '../Link/IstioObjectLink'; import { SimpleTable } from 'components/Table/SimpleTable'; -import { getIstioObjectGVK, gvkToString } from '../../utils/IstioConfigUtils'; +import { getGVKTypeString, getIstioObjectGVK } from '../../utils/IstioConfigUtils'; type IstioConfigCardProps = { items: IstioConfigItem[]; @@ -70,7 +70,7 @@ export const IstioConfigCard: React.FC = (props: IstioConf cells: [ {overviewLink(item)} diff --git a/plugin/src/kiali/components/IstioConfigPreview/IstioConfigPreview.tsx b/plugin/src/kiali/components/IstioConfigPreview/IstioConfigPreview.tsx index c53618dd..5cbaa297 100644 --- a/plugin/src/kiali/components/IstioConfigPreview/IstioConfigPreview.tsx +++ b/plugin/src/kiali/components/IstioConfigPreview/IstioConfigPreview.tsx @@ -35,7 +35,7 @@ import _ from 'lodash'; import { download } from 'utils/Common'; import { t } from 'utils/I18nUtils'; import { dump } from 'js-yaml'; -import { gvkToString } from '../../utils/IstioConfigUtils'; +import { getGVKTypeString } from '../../utils/IstioConfigUtils'; export type IstioConfigItem = | AuthorizationPolicy @@ -192,7 +192,7 @@ export class IstioConfigPreview extends React.Component { const gvks = _.uniq(list.map(item => item.objectGVK)); const itemsGrouped: ConfigPreviewItem[] = gvks.map(gvk => { - const filtered = list.filter(it => gvkToString(it.objectGVK) === gvkToString(gvk)); + const filtered = list.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString(gvk)); const item: ConfigPreviewItem = { objectGVK: gvk, title: filtered[0].title, items: [] }; filtered.map(f => item.items.push(f.items[0])); return item; diff --git a/plugin/src/kiali/components/IstioWizards/ServiceWizard.tsx b/plugin/src/kiali/components/IstioWizards/ServiceWizard.tsx index 1f3027d1..d23dd0d3 100644 --- a/plugin/src/kiali/components/IstioWizards/ServiceWizard.tsx +++ b/plugin/src/kiali/components/IstioWizards/ServiceWizard.tsx @@ -74,7 +74,7 @@ import { KialiIcon } from '../../config/KialiIcon'; import { ApiResponse } from 'types/Api'; import { t } from 'utils/I18nUtils'; import { dicIstioTypeToGVK } from '../../types/IstioConfigList'; -import { gvkToString } from '../../utils/IstioConfigUtils'; +import { getGVKTypeString } from '../../utils/IstioConfigUtils'; const emptyServiceWizardState = (fqdnServiceName: string): ServiceWizardState => { return { @@ -270,7 +270,7 @@ export class ServiceWizard extends React.Component { - const dr = items.filter(it => gvkToString(it.objectGVK) === gvkToString(dicIstioTypeToGVK['DestinationRule']))[0]; - const gw = items.filter(it => gvkToString(it.objectGVK) === gvkToString(dicIstioTypeToGVK['Gateway']))[0]; - const k8sgateway = items.filter( - it => gvkToString(it.objectGVK) === gvkToString(dicIstioTypeToGVK['K8sGateway']) - )[0]; - const pa = items.filter( - it => gvkToString(it.objectGVK) === gvkToString(dicIstioTypeToGVK['PeerAuthentication']) - )[0]; - const vs = items.filter(it => gvkToString(it.objectGVK) === gvkToString(dicIstioTypeToGVK['VirtualService']))[0]; - const k8shttproute = items.filter( - it => gvkToString(it.objectGVK) === gvkToString(dicIstioTypeToGVK['K8sHTTPRoute']) - )[0]; - const k8sgrpcroute = items.filter( - it => gvkToString(it.objectGVK) === gvkToString(dicIstioTypeToGVK['K8sGRPCRoute']) - )[0]; + const dr = items.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString('DestinationRule'))[0]; + const gw = items.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString('Gateway'))[0]; + const k8sgateway = items.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString('K8sGateway'))[0]; + const pa = items.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString('PeerAuthentication'))[0]; + const vs = items.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString('VirtualService'))[0]; + const k8shttproute = items.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString('K8sHTTPRoute'))[0]; + const k8sgrpcroute = items.filter(it => getGVKTypeString(it.objectGVK) === getGVKTypeString('K8sGRPCRoute'))[0]; const previews: WizardPreviews = { dr: dr ? (dr.items[0] as DestinationRule) : undefined, diff --git a/plugin/src/kiali/components/Link/IstioObjectLink.tsx b/plugin/src/kiali/components/Link/IstioObjectLink.tsx index 48bb06be..a9ca511b 100644 --- a/plugin/src/kiali/components/Link/IstioObjectLink.tsx +++ b/plugin/src/kiali/components/Link/IstioObjectLink.tsx @@ -10,7 +10,7 @@ import { KialiAppState } from '../../store/Store'; import { connect } from 'react-redux'; import { isParentKiosk, kioskContextMenuAction } from '../Kiosk/KioskActions'; import { GroupVersionKind } from '../../types/IstioObjects'; -import { gvkToString, kindToStringIncludeK8s } from '../../utils/IstioConfigUtils'; +import { getGVKTypeString, kindToStringIncludeK8s } from '../../utils/IstioConfigUtils'; const infoStyle = kialiStyle({ margin: '0 0 -0.125rem 0.5rem' @@ -62,7 +62,7 @@ export const GetIstioObjectUrl = ( export const ReferenceIstioObjectLink: React.FC = (props: ReferenceIstioObjectProps) => { const { name, namespace, cluster, objectGVK, subType } = props; - const badge = GVKToBadge[gvkToString(objectGVK)]; + const badge = GVKToBadge[getGVKTypeString(objectGVK)]; let showLink = true; let showTooltip = false; diff --git a/plugin/src/kiali/components/Table/ConfigTable.tsx b/plugin/src/kiali/components/Table/ConfigTable.tsx index f231ffa9..2ab35cdc 100644 --- a/plugin/src/kiali/components/Table/ConfigTable.tsx +++ b/plugin/src/kiali/components/Table/ConfigTable.tsx @@ -7,7 +7,7 @@ import { dump } from 'js-yaml'; import { yamlDumpOptions } from 'types/IstioConfigDetails'; interface ConfigTableProps { - configData?: { [key: string]: string }; + configData?: unknown; label: string; width: string; } diff --git a/plugin/src/kiali/components/VirtualList/Config.ts b/plugin/src/kiali/components/VirtualList/Config.ts index bb138c92..eb7fc7d6 100644 --- a/plugin/src/kiali/components/VirtualList/Config.ts +++ b/plugin/src/kiali/components/VirtualList/Config.ts @@ -10,7 +10,7 @@ import { NamespaceInfo } from '../../types/NamespaceInfo'; import { StatefulFiltersRef } from '../Filters/StatefulFilters'; import { PFBadges, PFBadgeType } from '../../components/Pf/PfBadges'; import { isGateway, isWaypoint } from '../../helpers/LabelFilterHelper'; -import { gvkToString } from '../../utils/IstioConfigUtils'; +import { getGVKTypeString } from '../../utils/IstioConfigUtils'; export type SortResource = AppListItem | WorkloadListItem | ServiceListItem; export type TResource = SortResource | IstioConfigItem; @@ -192,7 +192,7 @@ const istioType: ResourceType = { export const GVKToBadge: { [gvk: string]: PFBadgeType } = {}; Object.keys(dicIstioTypeToGVK).forEach(key => { - GVKToBadge[gvkToString(dicIstioTypeToGVK[key])] = PFBadges[key]; + GVKToBadge[getGVKTypeString(key)] = PFBadges[key]; }); export type Resource = { diff --git a/plugin/src/kiali/components/VirtualList/Renderers.tsx b/plugin/src/kiali/components/VirtualList/Renderers.tsx index 398fc1c6..b0a9014f 100644 --- a/plugin/src/kiali/components/VirtualList/Renderers.tsx +++ b/plugin/src/kiali/components/VirtualList/Renderers.tsx @@ -30,9 +30,9 @@ import { PFBadgeType, PFBadge, PFBadges } from 'components/Pf/PfBadges'; import { MissingLabel } from '../MissingLabel/MissingLabel'; import { MissingAuthPolicy } from 'components/MissingAuthPolicy/MissingAuthPolicy'; import { + getGVKTypeString, getIstioObjectGVK, getReconciliationCondition, - gvkToString, kindToStringIncludeK8s } from 'utils/IstioConfigUtils'; import { Label } from 'components/Label/Label'; @@ -144,7 +144,7 @@ export const details: Renderer key={ir.namespace ? `${ir.objectGVK.Group}.${ir.objectGVK.Kind}_${ir.name}_${ir.namespace}` : ir.name} style={{ marginBottom: '0.125rem' }} > - + `api/namespaces/${namespace}/apps/${app}/traces`, authenticate: 'api/authenticate', authInfo: 'api/auth/info', - canaryUpgradeStatus: () => 'api/mesh/canaries/status', clustersApps: () => `api/clusters/apps`, clustersHealth: () => `api/clusters/health`, clustersMetrics: () => `api/clusters/metrics`, @@ -130,6 +129,7 @@ const conf = { configValidations: () => `api/istio/validations`, controlPlaneMetrics: (namespace: string, controlPlane: string) => `api/namespaces/${namespace}/controlplanes/${controlPlane}/metrics`, + controlPlanes: 'api/mesh/controlplanes', crippledFeatures: 'api/crippled', customDashboard: (namespace: string, template: string) => `api/namespaces/${namespace}/customdashboard/${template}`, diff --git a/plugin/src/kiali/config/ServerConfig.ts b/plugin/src/kiali/config/ServerConfig.ts index c9198e4f..2bd67be3 100644 --- a/plugin/src/kiali/config/ServerConfig.ts +++ b/plugin/src/kiali/config/ServerConfig.ts @@ -169,6 +169,9 @@ export const setServerConfig = (cfg: ServerConfig): void => { homeCluster = getHomeCluster(serverConfig); isMultiCluster = isMC(); + if (!serverConfig.ambientEnabled) { + serverConfig.kialiFeatureFlags.uiDefaults.graph.traffic.ambient = 'none'; + } }; export const isIstioNamespace = (namespace: string): boolean => { diff --git a/plugin/src/kiali/helpers/GraphHelpers.tsx b/plugin/src/kiali/helpers/GraphHelpers.tsx new file mode 100644 index 00000000..bf955f09 --- /dev/null +++ b/plugin/src/kiali/helpers/GraphHelpers.tsx @@ -0,0 +1,192 @@ +import { Controller, Edge, GraphElement, isEdge, isNode, Node, NodeModel } from '@patternfly/react-topology'; +import { action } from 'mobx'; + +// 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 +export const descendents = (node: Node): Node[] => { + const result: Node[] = []; + if (!node.isGroup()) { + return result; + } + + const children = node.getChildren().filter(e => isNode(e)) as Node[]; + result.push(...children.filter(child => !child.isGroup())); + children.forEach(child => { + if (child.isGroup()) { + result.push(...descendents(child)); + } + }); + return result; +}; + +// setObserved executes the provided setter func in a single mobx action. The setter is expected to make at least one element update. +// like a call to elem.setData() or elem.setVisible(). PFT has these updates wrapped in a mobx observer and wants changes wrapper +// in a mobx action. To limit renders, batch several data updates in one setDataFunc execution. If you see a console warning like +// "[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed", +// then you're missing this wrapper. +export const setObserved = (setter: () => void): void => { + action(() => { + setter(); + })(); +}; + +export const elems = (c: Controller): { edges: Edge[]; nodes: Node[] } => { + const elems = c.getElements(); + return { + edges: elems.filter(e => isEdge(e)) as Edge[], + nodes: elems.filter(e => isNode(e)) as Node[] + }; +}; + +export const ancestors = (node: Node): GraphElement[] => { + const result: GraphElement[] = []; + while (node.hasParent()) { + const parent = node.getParent() as Node; + result.push(parent); + node = parent; + } + return result; +}; + +export type SelectOp = + | '=' + | '!=' + | '>' + | '<' + | '>=' + | '<=' + | '!*=' + | '!$=' + | '!^=' + | '*=' + | '$=' + | '^=' + | 'falsy' + | 'truthy'; +export type SelectExp = { + op?: SelectOp; + prop: string; + val?: any; +}; +export type SelectAnd = SelectExp[]; +export type SelectOr = SelectAnd[]; + +export const selectOr = (elems: GraphElement[], ors: SelectOr): GraphElement[] => { + let result = [] as GraphElement[]; + ors.forEach(ands => { + const andResult = selectAnd(elems, ands); + result = Array.from(new Set([...result, ...andResult])); + }); + return result; +}; + +export const selectAnd = (elems: GraphElement[], ands: SelectAnd): GraphElement[] => { + let result = elems; + ands.forEach(exp => (result = select(result, exp))); + return result; +}; + +export const select = (elems: GraphElement[], exp: SelectExp): GraphElement[] => { + return elems.filter(e => { + const propVal = e.getData()[exp.prop] || ''; + + switch (exp.op) { + case '!=': + return propVal !== exp.val; + case '<': + return propVal < exp.val; + case '>': + return propVal > exp.val; + case '>=': + return propVal >= exp.val; + case '<=': + return propVal <= exp.val; + case '!*=': + return !(propVal as string).includes(exp.val as string); + case '!$=': + return !(propVal as string).endsWith(exp.val as string); + case '!^=': + return !(propVal as string).startsWith(exp.val as string); + case '*=': + return (propVal as string).includes(exp.val as string); + case '$=': + return (propVal as string).endsWith(exp.val as string); + case '^=': + return (propVal as string).startsWith(exp.val as string); + case 'falsy': + return !propVal; + case 'truthy': + return !!propVal; + default: + return propVal === exp.val; + } + }); +}; + +export const edgesIn = (nodes: Node[], sourceNodes?: Node[]): Edge[] => { + const result = [] as Edge[]; + nodes.forEach(n => + result.push(...n.getTargetEdges().filter(e => !sourceNodes || sourceNodes.includes(e.getSource()))) + ); + return result; +}; + +export const edgesOut = (nodes: Node[], destNodes?: Node[]): Edge[] => { + const result = [] as Edge[]; + nodes.forEach(n => result.push(...n.getSourceEdges().filter(e => !destNodes || destNodes.includes(e.getTarget())))); + return result; +}; + +export const edgesInOut = (nodes: Node[]): Edge[] => { + const result = edgesIn(nodes); + result.push(...edgesOut(nodes)); + return Array.from(new Set(result)); +}; + +export const nodesIn = (nodes: Node[]): Node[] => { + const result = [] as Node[]; + edgesIn(nodes).forEach(e => result.push(e.getSource())); + return Array.from(new Set(result)); +}; + +export const nodesOut = (nodes: Node[]): Node[] => { + const result = [] as Node[]; + edgesOut(nodes).forEach(e => result.push(e.getTarget())); + return Array.from(new Set(result)); +}; + +export const predecessors = (node: Node, processed: GraphElement[]): GraphElement[] => { + let result = [] as GraphElement[]; + const targetEdges = node.getTargetEdges(); + const sourceNodes = targetEdges.map(e => e.getSource()); + result = result.concat(targetEdges, sourceNodes); + + sourceNodes.forEach(n => { + if (processed.indexOf(n) === -1) { + // Processed nodes is used to avoid infinite loops + processed = processed.concat(n); + result = result.concat(predecessors(n, processed)); + } + }); + + return result; +}; + +export const successors = (node: Node, processed: GraphElement[]): GraphElement[] => { + let result = [] as GraphElement[]; + const sourceEdges = node.getSourceEdges(); + const targetNodes = sourceEdges.map(e => e.getTarget()); + result = result.concat(sourceEdges, targetNodes); + targetNodes.forEach(n => { + if (processed.indexOf(n) === -1) { + // Processed nodes is used to avoid infinite loops + processed = processed.concat(n); + result = result.concat(successors(n, processed)); + } + }); + return result; +}; + +export const leafNodes = (nodes: Node[]): Node[] => { + return nodes.filter(n => n.getSourceEdges().length === 0); +}; diff --git a/plugin/src/kiali/hooks/services.ts b/plugin/src/kiali/hooks/services.ts index 32d320ce..4c2a17b5 100644 --- a/plugin/src/kiali/hooks/services.ts +++ b/plugin/src/kiali/hooks/services.ts @@ -7,8 +7,7 @@ import { filterAutogeneratedGateways, getGatewaysAsList, PeerAuthentication } fr import { DecoratedGraphNodeData, NodeType } from '../types/Graph'; import * as AlertUtils from '../utils/AlertUtils'; import { ApiError } from 'types/Api'; -import { dicIstioTypeToGVK } from '../types/IstioConfigList'; -import { gvkToString } from '../utils/IstioConfigUtils'; +import { getGVKTypeString } from '../utils/IstioConfigUtils'; type ServiceDetailHookInfo = readonly [ ServiceDetailsInfo | null, @@ -39,10 +38,10 @@ const useServiceDetail = ( setIsLoading(true); // Mark as loading let getDetailPromise = API.getServiceDetail(namespace, serviceName, false, cluster, duration); - let getGwPromise = API.getAllIstioConfigs([gvkToString(dicIstioTypeToGVK['Gateway'])], false, '', '', cluster); + let getGwPromise = API.getAllIstioConfigs([getGVKTypeString('Gateway')], false, '', '', cluster); let getPeerAuthsPromise = API.getIstioConfig( namespace, - [gvkToString(dicIstioTypeToGVK['PeerAuthentication'])], + [getGVKTypeString('PeerAuthentication')], false, '', '', @@ -53,9 +52,11 @@ const useServiceDetail = ( allPromise.promise .then(results => { setServiceDetails(results[0]); - setGateways(getGatewaysAsList(filterAutogeneratedGateways(results[1].data.gateways)).sort()); + setGateways( + getGatewaysAsList(filterAutogeneratedGateways(results[1].data.resources[getGVKTypeString('Gateway')])).sort() + ); - setPeerAuthentications(results[2].data.peerAuthentications); + setPeerAuthentications(results[2].data.resources[getGVKTypeString('PeerAuthentication')]); setFetchError(null); setIsLoading(false); }) diff --git a/plugin/src/kiali/locales/zh/translation.json b/plugin/src/kiali/locales/zh/translation.json index 8dd2a587..afe13dbc 100644 --- a/plugin/src/kiali/locales/zh/translation.json +++ b/plugin/src/kiali/locales/zh/translation.json @@ -299,6 +299,7 @@ "Switch language": "更换语言", "Switch to {{theme}} Mode": "Switch to {{theme}} Mode", "System Error": "System Error", + "Tag: {{tag}}": "Tag: {{tag}}", "TCP Connection": "TCP Connection", "TCP Traffic": "TCP Traffic", "TCP Traffic Shifting": "TCP 流量调整", diff --git a/plugin/src/kiali/pages/AppList/FiltersAndSorts.ts b/plugin/src/kiali/pages/AppList/FiltersAndSorts.ts index e096b222..a1126204 100644 --- a/plugin/src/kiali/pages/AppList/FiltersAndSorts.ts +++ b/plugin/src/kiali/pages/AppList/FiltersAndSorts.ts @@ -17,7 +17,7 @@ import { filterByLabel } from '../../helpers/LabelFilterHelper'; import { istioConfigTypeFilter } from '../IstioConfigList/FiltersAndSorts'; import { ObjectReference } from '../../types/IstioObjects'; import { serverConfig } from 'config'; -import { gvkToString, istioTypesToGVKString } from '../../utils/IstioConfigUtils'; +import { getGVKTypeString, istioTypesToGVKString } from '../../utils/IstioConfigUtils'; export const sortFields: SortField[] = [ { @@ -150,7 +150,7 @@ const filterByIstioSidecar = (items: AppListItem[], istioSidecar: boolean): AppL const filterByIstioType = (items: AppListItem[], istioTypes: string[]): AppListItem[] => { return items.filter( item => - item.istioReferences.filter(ref => istioTypesToGVKString(istioTypes).includes(gvkToString(ref.objectGVK))) + item.istioReferences.filter(ref => istioTypesToGVKString(istioTypes).includes(getGVKTypeString(ref.objectGVK))) .length !== 0 ); }; diff --git a/plugin/src/kiali/pages/Graph/GraphToolbar/GraphFindPF.tsx b/plugin/src/kiali/pages/Graph/GraphToolbar/GraphFindPF.tsx index 51e965f0..04c2ab5d 100644 --- a/plugin/src/kiali/pages/Graph/GraphToolbar/GraphFindPF.tsx +++ b/plugin/src/kiali/pages/Graph/GraphToolbar/GraphFindPF.tsx @@ -32,16 +32,9 @@ 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 { - descendents, - EdgeData, - elems, - NodeData, - SelectAnd, - SelectExp, - selectOr, - SelectOr -} from 'pages/GraphPF/GraphPFElems'; +import { EdgeData, NodeData } from 'pages/GraphPF/GraphPFElems'; +import { elems, SelectAnd, SelectExp, selectOr, SelectOr, setObserved } from 'helpers/GraphHelpers'; +import { descendents } from 'helpers/GraphHelpers'; import { isArray } from 'lodash'; import { graphLayout, LayoutType } from 'pages/GraphPF/GraphPF'; @@ -544,12 +537,16 @@ class GraphFindPFComponent extends React.Component { - e.setVisible(true); + private unhideElements = (g: Graph, elems: GraphElement[]): void => { + setObserved(() => { + elems.forEach(e => { + e.setVisible(true); - if (!e.hasParent()) { - g.appendChild(e); - } + if (!e.hasParent()) { + g.appendChild(e); + } + }); + }); }; private handleHide = (controller: Controller): void => { @@ -561,7 +558,7 @@ class GraphFindPFComponent extends React.Component {!options.nameOnly && ( diff --git a/plugin/src/kiali/pages/Mesh/target/TargetPanelConfigTable.tsx b/plugin/src/kiali/pages/Mesh/target/TargetPanelConfigTable.tsx index 9ec8855e..b9455f5d 100644 --- a/plugin/src/kiali/pages/Mesh/target/TargetPanelConfigTable.tsx +++ b/plugin/src/kiali/pages/Mesh/target/TargetPanelConfigTable.tsx @@ -6,7 +6,7 @@ import { yamlDumpOptions } from 'types/IstioConfigDetails'; import { ConfigButtonsTargetPanel } from '../../../components/Mesh/ConfigButtonsTargetPanel'; interface TargetPanelConfigTableProps { - configData: { [key: string]: string }; + configData: unknown; targetName: string; width: string; } diff --git a/plugin/src/kiali/pages/Mesh/target/TargetPanelControlPlane.tsx b/plugin/src/kiali/pages/Mesh/target/TargetPanelControlPlane.tsx index 7649834e..bfbe484d 100644 --- a/plugin/src/kiali/pages/Mesh/target/TargetPanelControlPlane.tsx +++ b/plugin/src/kiali/pages/Mesh/target/TargetPanelControlPlane.tsx @@ -10,7 +10,6 @@ import { } from './TargetPanelCommon'; import { Title, TitleSizes } from '@patternfly/react-core'; import { serverConfig } from 'config'; -import { CanaryUpgradeStatus } from 'types/IstioObjects'; import { NamespaceInfo, NamespaceStatus } from 'types/NamespaceInfo'; import { DirectionType } from 'pages/Overview/OverviewToolbar'; import { PromisesRegistry } from 'utils/CancelablePromises'; @@ -20,11 +19,8 @@ import { IstioMetricsOptions } from 'types/MetricsOptions'; import { computePrometheusRateParams } from 'services/Prometheus'; import { ApiError } from 'types/Api'; import { DEGRADED, FAILURE, HEALTHY, Health, NOT_READY } from 'types/Health'; -import * as AlertUtils from '../../../utils/AlertUtils'; -import { MessageType } from 'types/MessageCenter'; import { TLSStatus, nsWideMTLSStatus } from 'types/TLSStatus'; import * as FilterHelper from '../../../components/FilterList/FilterHelper'; -import { NodeData } from '../MeshElems'; import { ControlPlaneMetricsMap } from 'types/Metrics'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; @@ -38,17 +34,18 @@ import { CertsInfo } from 'types/CertsInfo'; import { IstioCertsInfo } from 'components/IstioCertsInfo/IstioCertsInfo'; import { TargetPanelControlPlaneMetrics } from './TargetPanelControlPlaneMetrics'; import { TargetPanelControlPlaneStatus } from './TargetPanelControlPlaneStatus'; +import { ControlPlane, IstiodNodeData, NodeTarget } from 'types/Mesh'; type TargetPanelControlPlaneProps = TargetPanelCommonProps & { meshStatus: string; minTLS: string; + target: NodeTarget; }; type TargetPanelControlPlaneState = { - canaryUpgradeStatus?: CanaryUpgradeStatus; certificates?: CertsInfo[]; controlPlaneMetrics?: ControlPlaneMetricsMap; - controlPlaneNode?: Node; + controlPlaneNode?: Node; loading: boolean; nsInfo?: NamespaceInfo; status?: NamespaceStatus; @@ -56,7 +53,6 @@ type TargetPanelControlPlaneState = { }; const defaultState: TargetPanelControlPlaneState = { - canaryUpgradeStatus: undefined, certificates: undefined, controlPlaneMetrics: undefined, controlPlaneNode: undefined, @@ -87,7 +83,7 @@ export class TargetPanelControlPlane extends React.Component< constructor(props: TargetPanelControlPlaneProps) { super(props); - const namespaceNode = this.props.target.elem as Node; + const namespaceNode = this.props.target.elem; this.state = { ...defaultState, controlPlaneNode: namespaceNode @@ -95,14 +91,12 @@ export class TargetPanelControlPlane extends React.Component< } static getDerivedStateFromProps: React.GetDerivedStateFromProps< - TargetPanelCommonProps, + TargetPanelControlPlaneProps, TargetPanelControlPlaneState > = (props, state) => { // if the target (e.g. namespaceBox) has changed, then init the state and set to loading. The loading // will actually be kicked off after the render (in componentDidMount/Update). - return props.target.elem !== state.controlPlaneNode - ? { controlPlaneNode: props.target.elem as Node, loading: true } - : null; + return props.target.elem !== state.controlPlaneNode ? { controlPlaneNode: props.target.elem, loading: true } : null; }; componentDidMount(): void { @@ -141,11 +135,10 @@ export class TargetPanelControlPlane extends React.Component< } const nsInfo = this.state.nsInfo; - const data = this.state.controlPlaneNode?.getData() as NodeData; + const data = this.state.controlPlaneNode?.getData()!; - // Controlplane infradata is structured: {config: configuration, revision: string} - const { config, revision } = data.infraData; - const parsedCm = config.ConfigMap ? this.getParsedYaml(config.ConfigMap) : ''; + const controlPlane: ControlPlane = data.infraData; + const parsedCm = controlPlane.config.configMap ? this.getParsedYaml(controlPlane.config.configMap) : ''; return (
{renderNodeHeader(data, {})}
+ {controlPlane.tag &&
{t('Tag: {{tag}}', { tag: controlPlane.tag.name })}
} +
{t('Version: {{version}}', { version: data.version || t(UNKNOWN) })}
- + @@ -206,38 +201,17 @@ export class TargetPanelControlPlane extends React.Component< private load = (): void => { this.promises.cancelAll(); - API.getNamespaces() - .then(result => { - const data = this.state.controlPlaneNode!.getData() as NodeData; - const cluster = data.cluster; - const namespace = data.namespace; - const nsInfo = result.data.find(ns => ns.cluster === cluster && ns.name === namespace); - - if (!nsInfo) { - AlertUtils.add(`Failed to find |${cluster}:${namespace}| in GetNamespaces() result`); - this.setState({ ...defaultState, loading: false }); - return; - } - - this.promises - .registerAll(`promises-${data.cluster}:${data.namespace}`, [ - this.fetchCanariesStatus(), - this.fetchHealthStatus(), - this.fetchMetrics(), - this.fetchTLS() - ]) - .then(_ => { - this.setState({ loading: false, nsInfo: nsInfo }); - }) - .catch(err => { - if (err.isCanceled) { - console.debug('TargetPanelNamespace: Ignore fetch error (canceled).'); - return; - } - - this.setState({ ...defaultState, loading: false }); - this.handleApiError('Could not loading target namespace panel', err); - }); + const data = this.state.controlPlaneNode!.getData()!; + + this.promises + .registerAll(`promises-${data.cluster}:${data.namespace}`, [ + this.fetchHealthStatus(), + this.fetchMetrics(), + this.fetchTLS(), + this.fetchNamespaceInfo() + ]) + .then(_ => { + this.setState({ loading: false }); }) .catch(err => { if (err.isCanceled) { @@ -246,32 +220,26 @@ export class TargetPanelControlPlane extends React.Component< } this.setState({ ...defaultState, loading: false }); - this.handleApiError('Could not fetch namespaces when loading target panel', err); + this.handleApiError('Could not loading target namespace panel', err); }); this.setState({ loading: true }); }; - private fetchCanariesStatus = async (): Promise => { - if (!this.isControlPlane()) { - return Promise.resolve(); - } + private fetchNamespaceInfo = async (): Promise => { + const data = this.state.controlPlaneNode!.getData()!; - return API.getCanaryUpgradeStatus() + return API.getNamespaceInfo(data.namespace, data.cluster) .then(response => { this.setState({ - canaryUpgradeStatus: { - namespacesPerRevision: response.data.namespacesPerRevision - } + nsInfo: response.data }); }) - .catch(error => { - AlertUtils.addError('Error fetching namespace canary upgrade status.', error, 'default', MessageType.ERROR); - }); + .catch(err => this.handleApiError('Could not fetch namespace info', err)); }; private fetchHealthStatus = async (): Promise => { - const data = this.state.controlPlaneNode!.getData() as NodeData; + const data = this.state.controlPlaneNode!.getData()!; return API.getClustersAppHealth(data.namespace, this.props.duration, data.cluster) .then(results => { @@ -318,7 +286,7 @@ export class TargetPanelControlPlane extends React.Component< step: rateParams.step }; - const data = this.state.controlPlaneNode!.getData() as NodeData; + const data = this.state.controlPlaneNode!.getData()!; return API.getControlPlaneMetrics(data.namespace, data.infraName, options, data.cluster) .then(rs => { @@ -342,7 +310,7 @@ export class TargetPanelControlPlane extends React.Component< return Promise.resolve(); } - const data = this.state.controlPlaneNode!.getData() as NodeData; + const data = this.state.controlPlaneNode!.getData()!; return API.getNamespaceTls(data.namespace, data.cluster) .then(rs => { @@ -358,7 +326,7 @@ export class TargetPanelControlPlane extends React.Component< }; private isControlPlane = (): boolean => { - const data = this.state.controlPlaneNode!.getData() as NodeData; + const data = this.state.controlPlaneNode!.getData()!; return data.namespace === serverConfig.istioNamespace; }; @@ -368,7 +336,7 @@ export class TargetPanelControlPlane extends React.Component< private renderCharts = (): React.ReactNode => { if (this.state.status) { - const data = this.state.controlPlaneNode!.getData() as NodeData; + const data = this.state.controlPlaneNode!.getData()!; const { thresholds } = data.infraData; return ( diff --git a/plugin/src/kiali/pages/Mesh/target/TargetPanelDataPlane.tsx b/plugin/src/kiali/pages/Mesh/target/TargetPanelDataPlane.tsx index 7ee25a82..4da7accc 100644 --- a/plugin/src/kiali/pages/Mesh/target/TargetPanelDataPlane.tsx +++ b/plugin/src/kiali/pages/Mesh/target/TargetPanelDataPlane.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { Node, NodeModel } from '@patternfly/react-topology'; import { TargetPanelCommonProps, nodeStyle, targetPanelStyle } from './TargetPanelCommon'; import { classes } from 'typestyle'; -import { MeshNodeData } from 'types/Mesh'; +import { DataPlaneNodeData, MeshNodeData, NodeTarget } from 'types/Mesh'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { PFBadge, PFBadges } from 'components/Pf/PfBadges'; import { Title, TitleSizes } from '@patternfly/react-core'; @@ -13,7 +12,11 @@ import { serverConfig } from 'config'; import { useKialiTranslation } from 'utils/I18nUtils'; import { ControlPlaneVersionBadge } from 'pages/Overview/ControlPlaneVersionBadge'; -export const TargetPanelDataPlane: React.FC = (props: TargetPanelCommonProps) => { +type TargetPanelDataPlaneProps = TargetPanelCommonProps & { + target: NodeTarget; +}; + +export const TargetPanelDataPlane: React.FC = props => { const [expanded, setExpanded] = React.useState([]); const { t } = useKialiTranslation(); @@ -48,13 +51,13 @@ export const TargetPanelDataPlane: React.FC = (props: Ta ); }; - const node = props.target?.elem as Node; + const node = props.target; if (!node) { return null; } - const data = node.getData() as MeshNodeData; + const data = node.elem.getData()!; return (
@@ -68,7 +71,7 @@ export const TargetPanelDataPlane: React.FC = (props: Ta - {(data.infraData as NamespaceInfo[]) + {data.infraData .filter(ns => ns.name !== serverConfig.istioNamespace) .sort((ns1, ns2) => (ns1.name < ns2.name ? -1 : 1)) .map((ns, i) => { @@ -94,7 +97,7 @@ export const TargetPanelDataPlane: React.FC = (props: Ta & { isExpanded: boolean; - namespaceData: { [key: string]: string }; + namespaceData: NamespaceInfo; targetCluster: string; targetNamespace: string; }; @@ -172,7 +172,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< {targetPanelHR} diff --git a/plugin/src/kiali/pages/Mesh/target/TargetPanelMesh.tsx b/plugin/src/kiali/pages/Mesh/target/TargetPanelMesh.tsx index ae6a2e98..041f706f 100644 --- a/plugin/src/kiali/pages/Mesh/target/TargetPanelMesh.tsx +++ b/plugin/src/kiali/pages/Mesh/target/TargetPanelMesh.tsx @@ -3,8 +3,8 @@ import { Node, NodeModel, Visualization } from '@patternfly/react-topology'; import { TargetPanelCommonProps, getTitle, renderNodeHeader, targetPanelStyle } from './TargetPanelCommon'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; -import { elems, selectAnd } from '../MeshElems'; -import { MeshAttr, MeshInfraType, MeshNodeData } from 'types/Mesh'; +import { elems, selectAnd } from 'helpers/GraphHelpers'; +import { DataPlaneNodeData, MeshAttr, MeshInfraType, MeshNodeData } from 'types/Mesh'; import { kialiStyle } from 'styles/StyleUtils'; import { useKialiTranslation } from 'utils/I18nUtils'; import { UNKNOWN } from 'types/Graph'; @@ -44,8 +44,8 @@ export const TargetPanelMesh: React.FC = (props: TargetPan {infraNodes .filter(node => node.getData().cluster === clusterData.cluster) .sort((in1, in2) => { - const data1 = in1.getData() as MeshNodeData; - const data2 = in2.getData() as MeshNodeData; + const data1 = in1.getData(); + const data2 = in2.getData(); if (data1.infraType === MeshInfraType.ISTIOD) { return -1; @@ -81,7 +81,7 @@ export const TargetPanelMesh: React.FC = (props: TargetPan ); }; - const renderDataPlaneSummary = (nodeData: MeshNodeData): React.ReactNode => { + const renderDataPlaneSummary = (nodeData: DataPlaneNodeData): React.ReactNode => { return (
{renderNodeHeader(nodeData, { nameOnly: true, smallSize: true })} @@ -90,7 +90,7 @@ export const TargetPanelMesh: React.FC = (props: TargetPan {nodeData.version &&
{t('Revision: {{revision}}', { revision: nodeData.version })}
} {t('{{count}} namespace', { - count: nodeData.infraData.length, + count: nodeData.infraData?.length, defaultValue_one: '{{count}} namespace', defaultValue_other: '{{count}} namespaces' })} diff --git a/plugin/src/kiali/pages/Mesh/target/TargetPanelNamespace.tsx b/plugin/src/kiali/pages/Mesh/target/TargetPanelNamespace.tsx index 7c9ad95c..51cdb321 100644 --- a/plugin/src/kiali/pages/Mesh/target/TargetPanelNamespace.tsx +++ b/plugin/src/kiali/pages/Mesh/target/TargetPanelNamespace.tsx @@ -1,21 +1,31 @@ import * as React from 'react'; -import { ElementModel, GraphElement, Node, NodeModel } from '@patternfly/react-topology'; +import { Node, NodeModel } from '@patternfly/react-topology'; import { kialiStyle } from 'styles/StyleUtils'; import { TargetPanelCommonProps, shouldRefreshData, targetPanelHR, targetPanelStyle } from './TargetPanelCommon'; import { PFBadge, PFBadges } from 'components/Pf/PfBadges'; -import { Card, CardBody, CardHeader, Label, Title, TitleSizes, Tooltip, TooltipPosition } from '@patternfly/react-core'; +import { + Card, + CardBody, + CardHeader, + Label, + List, + ListItem, + Title, + TitleSizes, + Tooltip, + TooltipPosition +} from '@patternfly/react-core'; import { Paths, serverConfig } from 'config'; -import { CanaryUpgradeStatus, ValidationStatus } from 'types/IstioObjects'; +import { ValidationStatus } from 'types/IstioObjects'; import { OverviewNamespaceAction, OverviewNamespaceActions } from 'pages/Overview/OverviewNamespaceActions'; import { NamespaceInfo, NamespaceStatus } from 'types/NamespaceInfo'; import { NamespaceMTLSStatus } from 'components/MTls/NamespaceMTLSStatus'; import { NamespaceStatuses } from 'pages/Overview/NamespaceStatuses'; import { DirectionType, OverviewType } from 'pages/Overview/OverviewToolbar'; import { PromisesRegistry } from 'utils/CancelablePromises'; -import { CanaryUpgradeProgress } from 'pages/Overview/CanaryUpgradeProgress'; +import { ControlPlaneDonut } from 'pages/Overview/ControlPlaneDonut'; import { isParentKiosk, kioskOverviewAction } from 'components/Kiosk/KioskActions'; import { Show } from 'pages/Overview/OverviewPage'; -import { ControlPlaneBadge } from 'pages/Overview/ControlPlaneBadge'; import { ControlPlaneVersionBadge } from 'pages/Overview/ControlPlaneVersionBadge'; import { AmbientBadge } from 'components/Ambient/AmbientBadge'; import { PFColors } from 'components/Pf/PfColors'; @@ -38,29 +48,31 @@ import { Metric } from 'types/Metrics'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { isRemoteCluster } from './TargetPanelControlPlane'; +import { BoxTarget, ControlPlane, NamespaceNodeData } from 'types/Mesh'; +import { MeshInfraType } from 'types/Mesh'; -type TargetPanelNamespaceProps = TargetPanelCommonProps; +type TargetPanelNamespaceProps = TargetPanelCommonProps & { + target: BoxTarget; +}; type TargetPanelNamespaceState = { - canaryUpgradeStatus?: CanaryUpgradeStatus; + controlPlanes?: ControlPlane[]; errorMetrics?: Metric[]; loading: boolean; metrics?: Metric[]; nsInfo?: NamespaceInfo; status?: NamespaceStatus; - targetCluster?: string; - targetNamespace?: string; - targetNode?: Node; + targetCluster: string; + targetNamespace: string; + targetNode: Node; tlsStatus?: TLSStatus; }; -const defaultState: TargetPanelNamespaceState = { - canaryUpgradeStatus: undefined, +const defaultState = { errorMetrics: undefined, loading: false, nsInfo: undefined, status: undefined, - targetNode: undefined, tlsStatus: undefined }; @@ -90,10 +102,22 @@ export class TargetPanelNamespace extends React.Component; - const data = (props.target.elem as GraphElement).getData(); + const targetNode = this.props.target.elem; + + const data = targetNode.getData()!; + + // Find the controlplanes nodes and pull out the controlplane + // object from infraData. The controlplane object has all the + // managed namespaces that is needed by elements on this page. + const controlPlanes: ControlPlane[] | undefined = props.target.elem + ?.getGraph() + .getData() + .meshData.elements.nodes?.filter(node => node.data.infraType === MeshInfraType.ISTIOD) + .map(node => node.data.infraData); + this.state = { ...defaultState, + controlPlanes: controlPlanes, targetCluster: data.cluster, targetNamespace: data.namespace, targetNode: targetNode @@ -101,19 +125,24 @@ export class TargetPanelNamespace extends React.Component).getData().cluster, - targetNamespace: (props.target.elem as GraphElement).getData().namespace, - loading: true - } as TargetPanelNamespaceState) - : null; + if (props.target.elem !== state.targetNode) { + const { cluster, namespace } = props.target.elem.getData()!; + return { + controlPlanes: state.controlPlanes, + targetNode: props.target.elem, + targetCluster: cluster, + targetNamespace: namespace, + nsInfo: state.nsInfo, + loading: true + }; + } + + return null; } componentDidMount(): void { @@ -135,10 +164,11 @@ export class TargetPanelNamespace extends React.Component ); @@ -183,18 +213,34 @@ export class TargetPanelNamespace extends React.Component - {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && ( - <> + {this.state.controlPlanes && ( +
{targetPanelHR} - - + +
)} - {this.props.istioAPIEnabled && ( - <> + {this.state.controlPlanes && ( +
{targetPanelHR} - {this.renderCharts()} - + Control Planes + + {this.state.controlPlanes + .sort((a, b) => a.istiodName.localeCompare(b.istiodName)) + .map(cp => ( + + {cp.istiodName} + + + Version: {cp.version ? cp.version.version : 'Unknown'} + + Revision: {cp.revision} + {cp.tag && Tag: {cp.tag.name}} + + + ))} + +
)} )} @@ -266,17 +312,7 @@ export class TargetPanelNamespace extends React.Component { - if (this.state.canaryUpgradeStatus) { - if (Object.keys(this.state.canaryUpgradeStatus.namespacesPerRevision).length > 1) { - return true; - } - } - - return false; - }; - - private getNamespaceActions = (nsInfo: NamespaceInfo): OverviewNamespaceAction[] => { + private getNamespaceActions = (): OverviewNamespaceAction[] => { // Today actions are fixed, but soon actions may depend of the state of a namespace // So we keep this wrapped in a showActions function. const namespaceActions: OverviewNamespaceAction[] = isParentKiosk(this.props.kiosk) @@ -344,190 +380,6 @@ export class TargetPanelNamespace extends React.Component console.log(`TODO: Enable Auto Injection [${ns}]`) - /* - this.setState({ - showTrafficPoliciesModal: true, - nsTarget: ns, - opTarget: 'enable', - kind: 'injection', - clusterTarget: nsInfo.cluster - }) - */ - }; - - const disableAction = { - 'data-test': `disable-${nsInfo.name}-namespace-sidecar-injection`, - isGroup: false, - isSeparator: false, - title: 'Disable Auto Injection', - action: (ns: string) => console.log(`TODO: Disable Auto Injection [${ns}]`) - /* - this.setState({ - showTrafficPoliciesModal: true, - nsTarget: ns, - opTarget: 'disable', - kind: 'injection', - clusterTarget: nsInfo.cluster - }) - */ - }; - - const removeAction = { - 'data-test': `remove-${nsInfo.name}-namespace-sidecar-injection`, - isGroup: false, - isSeparator: false, - title: 'Remove Auto Injection', - action: (ns: string) => console.log(`TODO: Remove Auto Injection [${ns}]`) - /* - this.setState({ - showTrafficPoliciesModal: true, - nsTarget: ns, - opTarget: 'remove', - kind: 'injection', - clusterTarget: nsInfo.cluster - }) - */ - }; - - if ( - nsInfo.labels && - ((nsInfo.labels[serverConfig.istioLabels.injectionLabelName] && - nsInfo.labels[serverConfig.istioLabels.injectionLabelName] === 'enabled') || - nsInfo.labels[serverConfig.istioLabels.injectionLabelRev]) - ) { - namespaceActions.push(disableAction); - namespaceActions.push(removeAction); - } else if ( - nsInfo.labels && - nsInfo.labels[serverConfig.istioLabels.injectionLabelName] && - nsInfo.labels[serverConfig.istioLabels.injectionLabelName] === 'disabled' - ) { - namespaceActions.push(enableAction); - namespaceActions.push(removeAction); - } else { - namespaceActions.push(enableAction); - } - } - - if (serverConfig.kialiFeatureFlags.istioUpgradeAction && this.hasCanaryUpgradeConfigured()) { - const revisionActions = Object.keys(this.state.canaryUpgradeStatus?.namespacesPerRevision || {}) - .filter(revision => !this.state.canaryUpgradeStatus!.namespacesPerRevision[revision].includes(nsInfo.name)) - .map(revision => ({ - isGroup: false, - isSeparator: false, - title: `Switch to ${revision} revision`, - action: (ns: string) => console.log(`TODO: Upgrade revision [${ns}]`) - /* - this.setState({ - opTarget: 'upgrade', - kind: 'canary', - nsTarget: ns, - showTrafficPoliciesModal: true, - clusterTarget: nsInfo.cluster - }) - */ - })); - - namespaceActions.push({ - isGroup: false, - isSeparator: true - }); - - revisionActions.forEach(action => { - namespaceActions.push(action); - }); - } - - const aps = nsInfo.istioConfig?.authorizationPolicies ?? []; - - const addAuthorizationAction = { - isGroup: false, - isSeparator: false, - title: `${aps.length === 0 ? 'Create ' : 'Update'} Traffic Policies`, - action: (ns: string) => console.log(`TODO: create traffic policies [${ns}]`) - /* - this.setState({ - opTarget: aps.length === 0 ? 'create' : 'update', - nsTarget: ns, - clusterTarget: nsInfo.cluster, - showTrafficPoliciesModal: true, - kind: 'policy' - }); - */ - }; - - const removeAuthorizationAction = { - isGroup: false, - isSeparator: false, - title: 'Delete Traffic Policies', - action: (ns: string) => console.log(`TODO: delete traffic policies [${ns}]`) - /* - this.setState({ - opTarget: 'delete', - nsTarget: ns, - showTrafficPoliciesModal: true, - kind: 'policy', - clusterTarget: nsInfo.cluster - }) - */ - }; - - if (this.props.istioAPIEnabled) { - namespaceActions.push({ - isGroup: false, - isSeparator: true - }); - - namespaceActions.push(addAuthorizationAction); - - if (aps.length > 0) { - namespaceActions.push(removeAuthorizationAction); - } - } - } else { - console.log(`TODO: grafana links`); - /* - if (this.state.grafanaLinks.length > 0) { - // Istio namespace will render external Grafana dashboards - namespaceActions.push({ - isGroup: false, - isSeparator: true - }); - - this.state.grafanaLinks.forEach(link => { - const grafanaDashboard = { - isGroup: false, - isSeparator: false, - isExternal: true, - title: link.name, - action: (_ns: string) => { - window.open(link.url, '_blank'); - this.load(); - } - }; - - namespaceActions.push(grafanaDashboard); - }); - */ - } - return namespaceActions; }; @@ -535,18 +387,9 @@ export class TargetPanelNamespace extends React.Component - {isControlPlane && } - - {isControlPlane && - this.hasCanaryUpgradeConfigured() && - this.state.canaryUpgradeStatus && - this.state.canaryUpgradeStatus.namespacesPerRevision && - Object.keys(this.state.canaryUpgradeStatus!.namespacesPerRevision).map( - revision => - !this.state.canaryUpgradeStatus!.namespacesPerRevision[revision].includes(ns.name) && ( - - ) - )} + {isControlPlane && ns.name !== serverConfig.istioNamespace && ( + + )} {isControlPlane && !this.props.istioAPIEnabled && (