From 27d2a29c14f93b057728411bc1589283db4e7300 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 23 Apr 2024 14:12:41 -0400 Subject: [PATCH 01/46] - replace old mesh page with new mesh page - remove undocumented spec.kiali_feature_flags.ui_defaults.mesh.impl - remove now-unused /api/clusters API --- config/config.go | 2 - frontend/src/components/Nav/Menu.tsx | 26 +-- frontend/src/config/Config.ts | 1 - frontend/src/pages/Mesh/old/OldMeshPage.tsx | 177 -------------------- frontend/src/routes.ts | 14 +- frontend/src/services/Api.ts | 6 +- handlers/mesh.go | 33 ---- routing/routes.go | 18 -- 8 files changed, 4 insertions(+), 273 deletions(-) delete mode 100644 frontend/src/pages/Mesh/old/OldMeshPage.tsx diff --git a/config/config.go b/config/config.go index e519450194..5f0d0f6fd4 100644 --- a/config/config.go +++ b/config/config.go @@ -487,7 +487,6 @@ type ListUIDefaults struct { type MeshUIDefaults 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"` // classic | topo | topo-as-overview } // Aggregation represents label's allowed aggregations, transformed from aggregation in MonitoringDashboard config resource @@ -845,7 +844,6 @@ func NewConfig() (c *Config) { Expression: "healthy", }, }, - Impl: "classic", }, MetricsInbound: MetricsDefaults{}, MetricsOutbound: MetricsDefaults{}, diff --git a/frontend/src/components/Nav/Menu.tsx b/frontend/src/components/Nav/Menu.tsx index f57cc667b5..54a95672d3 100644 --- a/frontend/src/components/Nav/Menu.tsx +++ b/frontend/src/components/Nav/Menu.tsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import { Nav, NavList, NavItem } from '@patternfly/react-core'; import { history } from '../../app/History'; import { navMenuItems } from '../../routes'; -import { homeCluster, serverConfig } from '../../config'; +import { serverConfig } from '../../config'; import { kialiStyle } from 'styles/StyleUtils'; import { ExternalServiceInfo } from '../../types/StatusState'; import { KialiIcon } from 'config/KialiIcon'; @@ -78,9 +78,6 @@ class MenuComponent extends React.Component { const allNavMenuItems = navMenuItems; const graphEnableCytoscape = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'pf'; const graphEnablePatternfly = serverConfig.kialiFeatureFlags.uiDefaults.graph.impl !== 'cy'; - const graphEnableMeshClassic = serverConfig.kialiFeatureFlags.uiDefaults.mesh.impl === 'classic'; - const graphEnableMeshGraph = serverConfig.kialiFeatureFlags.uiDefaults.mesh.impl !== 'classic'; - const graphEnableMeshOverview = serverConfig.kialiFeatureFlags.uiDefaults.mesh.impl === 'topo-as-overview'; const activeMenuItem = allNavMenuItems.find(item => { let isRoute = matchPath(location.pathname, { path: item.to, exact: true, strict: false }) ? true : false; @@ -96,18 +93,6 @@ class MenuComponent extends React.Component { return allNavMenuItems .filter(item => { - if (item.id === 'mesh_classic') { - return graphEnableMeshClassic && homeCluster?.name !== undefined; - } - - if (item.id === 'mesh_graph') { - return graphEnableMeshGraph; - } - - if (item.id === 'overview') { - return !graphEnableMeshOverview; - } - if (item.id === 'traffic_graph_cy') { return graphEnableCytoscape; } @@ -118,11 +103,6 @@ class MenuComponent extends React.Component { return true; }) - .sort((a, b): number => { - if (graphEnableMeshOverview && a.id === 'mesh_graph') return -1; - if (graphEnableMeshOverview && b.id === 'mesh_graph') return 1; - return 0; - }) .map(item => { let title = item.title; @@ -137,10 +117,6 @@ class MenuComponent extends React.Component { title = this.props.t('Traffic Graph'); } - if (item.id === 'mesh_classic' || item.id === 'mesh_graph') { - title = this.props.t('Mesh'); - } - return ( history.push(item.to)}> diff --git a/frontend/src/config/Config.ts b/frontend/src/config/Config.ts index 3a8bf0146d..c5744b9a63 100644 --- a/frontend/src/config/Config.ts +++ b/frontend/src/config/Config.ts @@ -121,7 +121,6 @@ const conf = { authenticate: 'api/authenticate', authInfo: 'api/auth/info', canaryUpgradeStatus: () => 'api/mesh/canaries/status', - clusters: 'api/clusters', clustersApps: () => `api/clusters/apps`, clustersHealth: () => `api/clusters/health`, clustersMetrics: () => `api/clusters/metrics`, diff --git a/frontend/src/pages/Mesh/old/OldMeshPage.tsx b/frontend/src/pages/Mesh/old/OldMeshPage.tsx deleted file mode 100644 index 4dedbc1a05..0000000000 --- a/frontend/src/pages/Mesh/old/OldMeshPage.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import * as React from 'react'; -import { EmptyState, EmptyStateBody, EmptyStateVariant, Tooltip, EmptyStateHeader } from '@patternfly/react-core'; -import { StarIcon } from '@patternfly/react-icons'; -import { IRow, SortByDirection } from '@patternfly/react-table'; -import { kialiStyle } from 'styles/StyleUtils'; -import { DefaultSecondaryMasthead } from '../../../components/DefaultSecondaryMasthead/DefaultSecondaryMasthead'; -import { RenderContent } from '../../../components/Nav/Page'; -import { RefreshButton } from '../../../components/Refresh/RefreshButton'; -import { getClusters } from '../../../services/Api'; -import { MeshClusters, MeshCluster } from '../../../types/Mesh'; -import { addError } from '../../../utils/AlertUtils'; -import { kialiIconDark, kialiIconLight } from 'config'; -import { KialiAppState } from 'store/Store'; -import { connect } from 'react-redux'; -import { Theme } from 'types/Common'; -import { SimpleTable, SortableTh } from 'components/SimpleTable'; - -const iconStyle = kialiStyle({ - width: '1.5rem', - marginRight: '0.5rem', - marginTop: '-0.125rem' -}); - -const containerStyle = kialiStyle({ padding: '1.25rem' }); - -type MeshPageProps = { - theme: string; -}; - -const OldMeshPageComponent: React.FunctionComponent = (props: MeshPageProps) => { - const [meshClustersList, setMeshClustersList] = React.useState(null as MeshClusters | null); - const [sortBy, setSortBy] = React.useState({ index: 0, direction: SortByDirection.asc }); - - React.useEffect(() => { - fetchMeshClusters(); - }, []); - - const columns: SortableTh[] = [ - { - title: 'Cluster Name', - width: 20, - sortable: true - }, - { - title: 'Network', - width: 10, - sortable: true - }, - { - title: 'Kiali', - width: 20, - sortable: false - }, - { - title: 'API Endpoint', - width: 20, - sortable: true - }, - { - title: 'Secret name', - width: 30, - sortable: true - } - ]; - - const buildKialiInstancesColumn = (cluster: MeshCluster, theme: string): React.ReactNode => { - if (!cluster.kialiInstances || cluster.kialiInstances.length === 0) { - return 'N / A'; - } - - const kialiIcon = theme === Theme.DARK ? kialiIconDark : kialiIconLight; - - return cluster.kialiInstances.map(instance => { - if (instance.url.length !== 0) { - return ( - -

- Kiali Icon - - {instance.namespace} {' / '} {instance.serviceName} - -

-
- ); - } else { - return ( -

- Kiali Icon - {`${instance.namespace} / ${instance.serviceName}`} -

- ); - } - }); - }; - - const buildTableRows = (): IRow[] => { - if (meshClustersList === null) { - return []; - } - - const sortAttributes = ['name', 'apiEndpoint', 'network', 'secretName']; - const sortByAttr = sortAttributes[sortBy.index]; - const sortedList = Array.from(meshClustersList).sort((a, b) => - a[sortByAttr].localeCompare(b[sortByAttr], undefined, { sensitivity: 'base' }) - ); - - const tableRows = sortedList.map((cluster: MeshCluster) => ({ - cells: [ - <> - {cluster.isKialiHome ? : null} {cluster.name} - , - cluster.network, - <>{buildKialiInstancesColumn(cluster, props.theme)}, - cluster.apiEndpoint, - cluster.secretName - ] - })); - - return sortBy.direction === SortByDirection.asc ? tableRows : tableRows.reverse(); - }; - - const fetchMeshClusters = async (): Promise => { - try { - const meshClusters = await getClusters(); - setMeshClustersList(meshClusters.data); - } catch (e) { - if (e instanceof Error) { - addError('Could not fetch the list of clusters that are part of the mesh.', e); - } - } - }; - - const onSortHandler = (_event: React.MouseEvent, index: number, direction: SortByDirection): void => { - setSortBy({ index, direction }); - }; - - const clusterRows = React.useMemo(buildTableRows, [meshClustersList, sortBy, props.theme]); - - return ( - <> - } - /> - - -
- - - {clusterRows.length === 0 ? ( - - - No clusters were discovered in your mesh. - - ) : null} -
-
- - ); -}; - -const mapStateToProps = (state: KialiAppState) => { - return { - theme: state.globalState.theme - }; -}; - -export const OldMeshPage = connect(mapStateToProps)(OldMeshPageComponent); diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 57c8acccd1..508c8ceb59 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -15,7 +15,6 @@ import { IstioConfigDetailsRoute } from 'routes/IstioConfigDetailsRoute'; import { IstioConfigNewRoute } from 'routes/IstioConfigNewRoute'; import { GraphRoutePF } from 'routes/GraphRoutePF'; import { GraphPagePF } from 'pages/GraphPF/GraphPagePF'; -import { OldMeshPage } from 'pages/Mesh/old/OldMeshPage'; import { i18n } from 'i18n'; /** @@ -71,14 +70,9 @@ const navMenuItems: MenuItem[] = [ to: '/tracing' }, { - id: 'mesh_graph', - title: i18n.t('Mesh [graph]'), + id: 'mesh', + title: i18n.t('Mesh'), to: '/mesh' - }, - { - id: 'mesh_classic', - title: i18n.t('Mesh [classic]'), - to: '/oldmesh' } ]; @@ -180,10 +174,6 @@ const pathRoutes: Path[] = [ { path: `/${Paths.MESH}`, component: MeshPage - }, - { - path: '/oldmesh', - component: OldMeshPage } ]; diff --git a/frontend/src/services/Api.ts b/frontend/src/services/Api.ts index 11a1f5982f..41213faaf4 100644 --- a/frontend/src/services/Api.ts +++ b/frontend/src/services/Api.ts @@ -40,7 +40,7 @@ import { } from '../types/IstioObjects'; import { ComponentStatus, IstiodResourceThresholds } from '../types/IstioStatus'; import { TracingInfo, TracingResponse, TracingSingleResponse } from '../types/TracingInfo'; -import { MeshClusters, MeshDefinition, MeshQuery } from '../types/Mesh'; +import { MeshDefinition, MeshQuery } from '../types/Mesh'; import { DashboardQuery, IstioMetricsOptions, MetricsStatsQuery } from '../types/MetricsOptions'; import { IstioMetricsMap, MetricsPerNamespace, MetricsStatsResult } from '../types/Metrics'; import { Namespace } from '../types/Namespace'; @@ -1196,10 +1196,6 @@ export const getMetricsStats = (queries: MetricsStatsQuery[]): Promise(HTTP_VERBS.POST, urls.metricsStats, {}, { queries: queries }); }; -export const getClusters = (): Promise> => { - return newRequest(HTTP_VERBS.GET, urls.clusters, {}, {}); -}; - export function deleteServiceTrafficRouting( virtualServices: VirtualService[], destinationRules: DestinationRuleC[], diff --git a/handlers/mesh.go b/handlers/mesh.go index acd3fcfaf9..c9ae16f487 100644 --- a/handlers/mesh.go +++ b/handlers/mesh.go @@ -4,44 +4,11 @@ import ( "fmt" "net/http" - "github.com/kiali/kiali/business" "github.com/kiali/kiali/config" - "github.com/kiali/kiali/kubernetes" - "github.com/kiali/kiali/kubernetes/cache" "github.com/kiali/kiali/mesh" "github.com/kiali/kiali/mesh/api" - "github.com/kiali/kiali/prometheus" - "github.com/kiali/kiali/tracing" ) -// GetClusters writes to the HTTP response a JSON document with the -// list of clusters that are part of the mesh when multi-cluster is enabled. If -// multi-cluster is not enabled in the control plane, this handler may provide -// erroneous data. -func GetClusters(conf *config.Config, kialiCache cache.KialiCache, clientFactory kubernetes.ClientFactory, prom prometheus.ClientInterface, traceClientLoader func() tracing.ClientInterface, cpm business.ControlPlaneMonitor) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - authInfo, err := getAuthInfo(r) - if err != nil { - RespondWithError(w, http.StatusInternalServerError, err.Error()) - return - } - - layer, err := business.NewLayer(conf, kialiCache, clientFactory, prom, traceClientLoader(), cpm, authInfo) - if err != nil { - RespondWithError(w, http.StatusInternalServerError, err.Error()) - return - } - - meshClusters, err := layer.Mesh.GetClusters() - if err != nil { - RespondWithError(w, http.StatusServiceUnavailable, "Cannot fetch mesh clusters: "+err.Error()) - return - } - - RespondWithJSON(w, http.StatusOK, meshClusters) - } -} - func OutboundTrafficPolicyMode(w http.ResponseWriter, r *http.Request) { business, err := getBusiness(r) if err != nil { diff --git a/routing/routes.go b/routing/routes.go index c4887d2c39..2e6936456a 100644 --- a/routing/routes.go +++ b/routing/routes.go @@ -1499,24 +1499,6 @@ func NewRoutes( HandlerFunc: handlers.MetricsStats, Authenticated: true, }, - // swagger:route GET /api/clusters - // --- - // Endpoint to get the list of the clusters that are hosting the service mesh. - // Produces: - // - application/json - // - // Schemes: http, https - // - // responses: - // 500: internalError - // 200: clustersResponse - { - "GetClusters", - "GET", - "/api/clusters", - handlers.GetClusters(conf, kialiCache, clientFactory, prom, traceClientLoader, cpm), - true, - }, // swagger:route GET /api/mesh/outbound_traffic_policy/mode // --- // Endpoint to get the OutboundTrafficPolicy Mode configured in the service mesh. From 471ae22cd143a343de43e234ab32af2c4fba5eb4 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Wed, 24 Apr 2024 09:35:49 -0400 Subject: [PATCH 02/46] WIP - restoring overview control plane card to mostly standard namespace behavior --- frontend/public/locales/zh/translation.json | 2 -- .../Overview/OverviewCardSparklineCharts.tsx | 25 +++++++------ frontend/src/pages/Overview/OverviewPage.tsx | 35 ++++++------------- frontend/src/pages/Overview/Sorts.ts | 17 ++------- 4 files changed, 27 insertions(+), 52 deletions(-) diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json index b4c672dd54..cf1e00eb3d 100644 --- a/frontend/public/locales/zh/translation.json +++ b/frontend/public/locales/zh/translation.json @@ -61,8 +61,6 @@ "Mb (Threshold)": "Mb (Threshold)", "Memory": "内存", "Mesh": "网格", - "Mesh [classic]": "网格 [classic]", - "Mesh [graph]": "网格 [graph]", "Mesh page": "Mesh page", "Min TLS version": "最低TLS版本", "More info at": "More info at", diff --git a/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx b/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx index 462f85c8ab..83b3db43d6 100644 --- a/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx +++ b/frontend/src/pages/Overview/OverviewCardSparklineCharts.tsx @@ -27,7 +27,7 @@ type Props = ReduxProps & { const OverviewCardSparklineChartsComponent: React.FC = (props: Props) => { return ( <> - {props.name !== serverConfig.istioNamespace && ( + {(props.name !== serverConfig.istioNamespace || !props.controlPlaneMetrics) && ( = (props: Props) => /> )} - {props.name === serverConfig.istioNamespace && props.istioAPIEnabled && !isRemoteCluster(props.annotations) && ( - - )} + {props.name === serverConfig.istioNamespace && + props.controlPlaneMetrics && + props.istioAPIEnabled && + !isRemoteCluster(props.annotations) && ( + + )} ); }; diff --git a/frontend/src/pages/Overview/OverviewPage.tsx b/frontend/src/pages/Overview/OverviewPage.tsx index 104584960a..f7eaa85a4f 100644 --- a/frontend/src/pages/Overview/OverviewPage.tsx +++ b/frontend/src/pages/Overview/OverviewPage.tsx @@ -68,7 +68,6 @@ import { isParentKiosk, kioskOverviewAction } from '../../components/Kiosk/Kiosk import { ValidationSummaryLink } from '../../components/Link/ValidationSummaryLink'; import { ControlPlaneBadge } from './ControlPlaneBadge'; import { OverviewStatus } from './OverviewStatus'; -import { ControlPlaneNamespaceStatus } from './ControlPlaneNamespaceStatus'; import { IstiodResourceThresholds } from 'types/IstioStatus'; import { TLSInfo } from 'components/Overview/TLSInfo'; import { CanaryUpgradeProgress } from './CanaryUpgradeProgress'; @@ -102,11 +101,13 @@ const cardGridStyle = kialiStyle({ marginBottom: '0.5rem' }); +/* const cardControlPlaneGridStyle = kialiStyle({ textAlign: 'center', marginTop: 0, marginBottom: '0.5rem' }); +*/ const emptyStateStyle = kialiStyle({ height: '300px', @@ -256,8 +257,8 @@ export class OverviewPageComponent extends React.Component errorMetrics: previous ? previous.errorMetrics : undefined, validations: previous ? previous.validations : undefined, labels: ns.labels, - annotations: ns.annotations, - controlPlaneMetrics: previous ? previous.controlPlaneMetrics : undefined + annotations: ns.annotations + //controlPlaneMetrics: previous ? previous.controlPlaneMetrics : undefined }; }); @@ -445,6 +446,7 @@ export class OverviewPageComponent extends React.Component nsInfo.metrics = rs.request_count; nsInfo.errorMetrics = rs.request_error_count; + /* if (nsInfo.name === serverConfig.istioNamespace) { nsInfo.controlPlaneMetrics = { istiod_proxy_time: rs.pilot_proxy_convergence_time, @@ -454,6 +456,7 @@ export class OverviewPageComponent extends React.Component istiod_process_mem: rs.process_resident_memory_bytes }; } + */ } }); }) @@ -1085,7 +1088,7 @@ export class OverviewPageComponent extends React.Component sm={ ns.name === serverConfig.istioNamespace && this.state.displayMode === OverviewDisplayMode.EXPAND && - (this.props.istioAPIEnabled || this.hasCanaryUpgradeConfigured()) + this.hasCanaryUpgradeConfigured() ? isRemoteCluster(ns.annotations) ? rlg : lg @@ -1094,7 +1097,7 @@ export class OverviewPageComponent extends React.Component md={ ns.name === serverConfig.istioNamespace && this.state.displayMode === OverviewDisplayMode.EXPAND && - (this.props.istioAPIEnabled || this.hasCanaryUpgradeConfigured()) + this.hasCanaryUpgradeConfigured() ? isRemoteCluster(ns.annotations) ? rlg : lg @@ -1106,7 +1109,7 @@ export class OverviewPageComponent extends React.Component > !isRemoteCluster(ns.annotations) && this.state.displayMode === OverviewDisplayMode.EXPAND && ( - + {this.renderLabels(ns)}
@@ -1172,22 +1175,6 @@ export class OverviewPageComponent extends React.Component type={this.state.type} /> )} - - {this.state.displayMode === OverviewDisplayMode.EXPAND && ( - - )} - - {this.state.displayMode === OverviewDisplayMode.EXPAND && ( - - )} {ns.name === serverConfig.istioNamespace && ( @@ -1366,7 +1353,7 @@ export class OverviewPageComponent extends React.Component direction={this.state.direction} metrics={ns.metrics} errorMetrics={ns.errorMetrics} - controlPlaneMetrics={ns.controlPlaneMetrics} + // controlPlaneMetrics={ns.controlPlaneMetrics} istiodResourceThresholds={this.state.istiodResourceThresholds} /> ); diff --git a/frontend/src/pages/Overview/Sorts.ts b/frontend/src/pages/Overview/Sorts.ts index 2197564e7f..ed2573835f 100644 --- a/frontend/src/pages/Overview/Sorts.ts +++ b/frontend/src/pages/Overview/Sorts.ts @@ -1,7 +1,5 @@ -import { serverConfig } from 'config'; import { SortField } from '../../types/SortFilters'; import { NamespaceInfo } from '../../types/NamespaceInfo'; -import { isRemoteCluster } from './OverviewCardControlPlaneNamespace'; import { i18n } from 'i18n'; export const sortFields: SortField[] = [ @@ -118,17 +116,6 @@ export const sortFunc = ( sortField: SortField, isAscending: boolean ): NamespaceInfo[] => { - const sortedNamespaces = allNamespaces - .filter(ns => ns.name !== serverConfig.istioNamespace) - .sort(isAscending ? sortField.compare : (a, b) => sortField.compare(b, a)); - - // remote cluster control planes should be listed after primary - const remoteControlPlanes = allNamespaces.filter( - ns => ns.name === serverConfig.istioNamespace && isRemoteCluster(ns.annotations) - ); - - return allNamespaces - .filter(ns => ns.name === serverConfig.istioNamespace && !isRemoteCluster(ns.annotations)) - .concat(remoteControlPlanes) - .concat(sortedNamespaces); + const sortedNamespaces = allNamespaces.sort(isAscending ? sortField.compare : (a, b) => sortField.compare(b, a)); + return sortedNamespaces; }; From f02630aa4a5cc8108792af1a0e1200012244f52c Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Fri, 26 Apr 2024 09:03:35 -0400 Subject: [PATCH 03/46] Add mesh page link to control plane badge --- .../src/pages/Overview/ControlPlaneBadge.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/Overview/ControlPlaneBadge.tsx b/frontend/src/pages/Overview/ControlPlaneBadge.tsx index 9aefa1fd4d..00273ccd7a 100644 --- a/frontend/src/pages/Overview/ControlPlaneBadge.tsx +++ b/frontend/src/pages/Overview/ControlPlaneBadge.tsx @@ -1,12 +1,13 @@ -import { Label } from '@patternfly/react-core'; +import { Label, Tooltip } from '@patternfly/react-core'; import * as React from 'react'; import { IstioStatusInline } from '../../components/IstioStatus/IstioStatusInline'; -import { serverConfig } from '../../config'; +import { config, serverConfig } from '../../config'; import { AmbientBadge } from '../../components/Ambient/AmbientBadge'; import { RemoteClusterBadge } from './RemoteClusterBadge'; import { isRemoteCluster } from './OverviewCardControlPlaneNamespace'; import { useTranslation } from 'react-i18next'; import { I18N_NAMESPACE } from 'types/Common'; +import { Link } from 'react-router-dom'; type Props = { annotations?: { [key: string]: string }; @@ -19,9 +20,17 @@ export const ControlPlaneBadge: React.FC = (props: Props) => { // so don't display istio status badge for those. return ( <> - + + {config.about.mesh.linkText} + + } + > + + {isRemoteCluster(props.annotations) && } From 808f7b3da3d73e1d103910c0eb6f846a299774fb Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Mon, 29 Apr 2024 14:28:08 -0400 Subject: [PATCH 04/46] Clean up exposure of special "_external_" deployment name --- .../pages/Mesh/target/TargetPanelCluster.tsx | 85 +++++++++------ .../src/pages/Mesh/target/TargetPanelNode.tsx | 102 ++++++++++-------- frontend/src/types/Mesh.ts | 6 ++ 3 files changed, 115 insertions(+), 78 deletions(-) diff --git a/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx b/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx index e047a3b884..f915e92412 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx @@ -14,16 +14,20 @@ import { targetPanelWidth } from './TargetPanelCommon'; import { kialiIconDark, kialiIconLight } from 'config'; -import { KialiInstance, MeshAttr } from 'types/Mesh'; -import { Theme } from 'types/Common'; +import { KialiInstance, MeshNodeData, isExternal } from 'types/Mesh'; +import { I18N_NAMESPACE, Theme } from 'types/Common'; import { PromisesRegistry } from 'utils/CancelablePromises'; import * as API from '../../../services/Api'; import * as FilterHelper from '../../../components/FilterList/FilterHelper'; import { ApiError } from 'types/Api'; import { KialiIcon } from 'config/KialiIcon'; -import { Tooltip } from '@patternfly/react-core'; +import { TitleSizes, Tooltip } from '@patternfly/react-core'; import { classes } from 'typestyle'; -import { UNKNOWN } from 'types/Graph'; +import { descendents } from '../MeshElems'; +import { renderNodeHeader } from './TargetPanelNode'; +import { WithTranslation, withTranslation } from 'react-i18next'; + +type TargetPanelClusterProps = WithTranslation & TargetPanelCommonProps; type TargetPanelClusterState = { clusterNode?: Node; @@ -40,7 +44,7 @@ const kialiIconStyle = kialiStyle({ marginRight: '0.25rem' }); -export class TargetPanelCluster extends React.Component { +class TargetPanelClusterComponent extends React.Component { static readonly panelStyle = { backgroundColor: PFColors.BackgroundColor100, height: '100%', @@ -52,28 +56,28 @@ export class TargetPanelCluster extends React.Component; this.state = { ...defaultState, clusterNode: clusterNode }; } - static getDerivedStateFromProps( + static getDerivedStateFromProps: React.GetDerivedStateFromProps = ( props: TargetPanelCommonProps, state: TargetPanelClusterState - ): TargetPanelClusterState | null { + ) => { // if the target (i.e. clusterBox) has changed, then init the state return props.target.elem !== state.clusterNode ? ({ clusterNode: props.target.elem, loading: true } as TargetPanelClusterState) : null; - } + }; componentDidMount() { this.load(); } - componentDidUpdate(prevProps: TargetPanelCommonProps) { + componentDidUpdate(prevProps: TargetPanelClusterProps) { if (shouldRefreshData(prevProps, this.props)) { this.load(); } @@ -88,42 +92,57 @@ export class TargetPanelCluster extends React.Component
{clusterData.isKialiHome && ( - + )} {clusterData.name}
-
- {clusterData.accessible && this.renderKialiLinks(clusterData.kialiInstances)} - {version && ( - <> - {`Version: `} - {version} -
- - )} - {`Network: `} - {clusterData.network ? clusterData.network : 'n/a'} -
- {`API Endpoint: `} - {clusterData.apiEndpoint ? clusterData.apiEndpoint : 'n/a'} -
- {`Secret Name: `} - {clusterData.secretName ? clusterData.secretName : 'n/a'} -
+ {isExternal(data.cluster) ? ( +
+ {descendents(this.state.clusterNode) + .sort((n1, n2) => { + const name1 = (n1.getData() as MeshNodeData).infraName.toLowerCase(); + const name2 = (n2.getData() as MeshNodeData).infraName.toLowerCase(); + return name1 < name2 ? -1 : 1; + }) + .map(n => { + return renderNodeHeader(n.getData() as MeshNodeData, this.props.t, true, TitleSizes.md); + })} +
+ ) : ( +
+ {clusterData.accessible && this.renderKialiLinks(clusterData.kialiInstances)} + {version && ( + <> + {`${this.props.t('Version')}: `} + {version} +
+ + )} + {`${this.props.t('Network')}: `} + {clusterData.network ? clusterData.network : 'n/a'} +
+ {`${this.props.t('API Endpoint')}: `} + {clusterData.apiEndpoint ? clusterData.apiEndpoint : 'n/a'} +
+ {`${this.props.t('Secret Name')}: `} + {clusterData.secretName ? clusterData.secretName : 'n/a'} +
+ )}
); } @@ -178,3 +197,5 @@ export class TargetPanelCluster extends React.Component + + <span className={nodeStyle}> + <PFBadge badge={pfBadge} size="global" /> + {data.infraName} + {!nameOnly && getHealthStatus(data, t)} + </span> + + {!nameOnly && ( + <> + + + {data.namespace} + + + + {data.cluster} + + + )} + + ); +} + class TargetPanelNodeComponent extends React.Component { constructor(props: TargetPanelNodeProps) { super(props); @@ -65,7 +119,7 @@ class TargetPanelNodeComponent extends React.Component -
{this.renderNodeHeader(data)}
+
{renderNodeHeader(data, this.props.t, isExternal(data.cluster))}
{data.version && (
@@ -79,50 +133,6 @@ class TargetPanelNodeComponent extends React.Component ); } - - private renderNodeHeader = (data: MeshNodeData): React.ReactNode => { - let pfBadge = PFBadges.Unknown; - - switch (data.infraType) { - case MeshInfraType.CLUSTER: - pfBadge = PFBadges.Cluster; - break; - case MeshInfraType.GRAFANA: - pfBadge = PFBadges.Grafana; - break; - case MeshInfraType.KIALI: - pfBadge = PFBadges.Kiali; - break; - case MeshInfraType.METRIC_STORE: - pfBadge = PFBadges.MetricStore; - break; - case MeshInfraType.TRACE_STORE: - pfBadge = PFBadges.TraceStore; - break; - default: - console.warn(`MeshElems: Unexpected infraType [${data.infraType}] `); - } - - return ( - - - <span className={nodeStyle}> - <PFBadge badge={pfBadge} size="global" /> - {data.infraName} - {getHealthStatus(data, this.props.t)} - </span> - - - - {data.namespace} - - - - {data.cluster} - - - ); - }; } export const TargetPanelNode = withTranslation(I18N_NAMESPACE)(TargetPanelNodeComponent); diff --git a/frontend/src/types/Mesh.ts b/frontend/src/types/Mesh.ts index 0265e708fe..71d5bf230b 100644 --- a/frontend/src/types/Mesh.ts +++ b/frontend/src/types/Mesh.ts @@ -151,3 +151,9 @@ export const MeshAttr = { nodeType: 'nodeType', version: 'version' }; + +// determine if the infra is deployed externally, typically +// tested against the clusterName. +export function isExternal(name): boolean { + return name === '_external_'; +} From 8f98e18a9433185b278cb6f38caa069b90eaf6dc Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Mon, 29 Apr 2024 15:35:22 -0400 Subject: [PATCH 05/46] - Limit the Mesh Tour to what is currently offered - Add in the actual "Mesh" tour stop --- frontend/src/pages/Mesh/MeshHelpTour.tsx | 31 ++++++++++--------- .../src/pages/Mesh/target/TargetPanel.tsx | 3 +- .../src/pages/Mesh/toolbar/MeshShortcuts.tsx | 6 ++-- .../src/pages/Mesh/toolbar/MeshToolbar.tsx | 4 ++- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/Mesh/MeshHelpTour.tsx b/frontend/src/pages/Mesh/MeshHelpTour.tsx index 98dfd3976c..1f8e0f5b0f 100644 --- a/frontend/src/pages/Mesh/MeshHelpTour.tsx +++ b/frontend/src/pages/Mesh/MeshHelpTour.tsx @@ -20,12 +20,6 @@ export const MeshTourStops: { [name: string]: TourStopInfo } = { 'Highlight or Hide mesh elements via typed expressions. Click the dropdown for preset Find or Hide expressions. Click the Find/Hide help icon for details on the expression language.', position: PopoverPosition.bottom }, - Mesh: { - name: 'Mesh', - description: 'Click on a node or edge to see its summary and emphasize its end-to-end paths.', - position: PopoverPosition.left, - distance: 250 - }, Layout: { name: 'Layout selection', description: @@ -37,10 +31,11 @@ export const MeshTourStops: { [name: string]: TourStopInfo } = { description: 'Display the legend to learn about what the different shapes, colors and backgrounds mean.', position: PopoverPosition.rightEnd }, - Refresh: { - name: 'Refresh', - description: 'Select how often to refresh the mesh topology.', - position: PopoverPosition.bottomEnd + Mesh: { + name: 'Mesh', + description: 'Click on a node or edge to see its summary and emphasize its end-to-end paths.', + position: PopoverPosition.left, + distance: 250 }, Shortcuts: { name: 'Shortcuts', @@ -51,6 +46,12 @@ export const MeshTourStops: { [name: string]: TourStopInfo } = { name: 'Side Panel', description: 'The Side Panel shows details about the currently selected node or edge, otherwise the whole mesh.', position: PopoverPosition.left + }, + TimeRange: { + name: 'Time Range', + description: + 'Select how often to refresh the mesh and how much historical metric data is used for metric charts. For example "Last 5m" means use the most recent 5 minutes of request metric data.', + position: PopoverPosition.bottomEnd } }; @@ -58,13 +59,13 @@ export const MeshTour: TourInfo = { name: 'MeshTour', stops: [ MeshTourStops.Shortcuts, - MeshTourStops.Display, + // MeshTourStops.Display, MeshTourStops.Find, - MeshTourStops.Refresh, + MeshTourStops.TimeRange, MeshTourStops.Mesh, - MeshTourStops.ContextualMenu, MeshTourStops.TargetPanel, - MeshTourStops.Layout, - MeshTourStops.Legend + // MeshTourStops.ContextualMenu, + MeshTourStops.Layout + // MeshTourStops.Legend ] }; diff --git a/frontend/src/pages/Mesh/target/TargetPanel.tsx b/frontend/src/pages/Mesh/target/TargetPanel.tsx index 2944a78f83..4054bc0993 100644 --- a/frontend/src/pages/Mesh/target/TargetPanel.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanel.tsx @@ -88,9 +88,10 @@ class TargetPanelComponent extends React.Component +
diff --git a/frontend/src/pages/Mesh/toolbar/MeshShortcuts.tsx b/frontend/src/pages/Mesh/toolbar/MeshShortcuts.tsx index eb82549ed0..a7b350e3f4 100644 --- a/frontend/src/pages/Mesh/toolbar/MeshShortcuts.tsx +++ b/frontend/src/pages/Mesh/toolbar/MeshShortcuts.tsx @@ -9,9 +9,9 @@ interface Shortcut { 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: 'Shift + Drag', description: 'Select zoom area' }, + // { shortcut: 'Right click', description: 'Contextual menu on nodes' }, + { shortcut: 'Single click', description: 'Details in side panel on nodes' } ]; const makeShortcut = (shortcut: Shortcut): JSX.Element => { diff --git a/frontend/src/pages/Mesh/toolbar/MeshToolbar.tsx b/frontend/src/pages/Mesh/toolbar/MeshToolbar.tsx index 01698657b4..d3753cfb67 100644 --- a/frontend/src/pages/Mesh/toolbar/MeshToolbar.tsx +++ b/frontend/src/pages/Mesh/toolbar/MeshToolbar.tsx @@ -69,7 +69,9 @@ class MeshToolbarComponent extends React.PureComponent { - + + + From bad7a9ae0398b10b62216e676f8f1f93681d47f1 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 30 Apr 2024 10:25:04 -0400 Subject: [PATCH 06/46] Apply some refresh/resize mesh page learnings to GraphPF. --- frontend/src/pages/GraphPF/GraphPF.tsx | 248 ++++++++++++++----------- 1 file changed, 138 insertions(+), 110 deletions(-) diff --git a/frontend/src/pages/GraphPF/GraphPF.tsx b/frontend/src/pages/GraphPF/GraphPF.tsx index 69d39c13f4..03a7cbb70d 100644 --- a/frontend/src/pages/GraphPF/GraphPF.tsx +++ b/frontend/src/pages/GraphPF/GraphPF.tsx @@ -1,4 +1,5 @@ import { Bullseye, Spinner } from '@patternfly/react-core'; +import ReactResizeDetector from 'react-resize-detector'; import { LongArrowAltRightIcon, TopologyIcon, MapIcon } from '@patternfly/react-icons'; import { Controller, @@ -214,7 +215,17 @@ const TopologyContent: React.FC<{ highlighter.setSelectedId(undefined); updateSummary({ isPF: true, summaryType: 'graph', summaryTarget: controller } as GraphEvent); } - }, [updateSummary, selectedIds, highlighter, controller, isMiniGraph, onEdgeTap, onNodeTap, setSelectedIds]); + }, [ + controller, + graphData, + highlighter, + isMiniGraph, + onEdgeTap, + onNodeTap, + selectedIds, + setSelectedIds, + updateSummary + ]); // // TraceOverlay State @@ -244,6 +255,20 @@ const TopologyContent: React.FC<{ } }, [controller]); + // resize handling + const handleResize = React.useCallback(() => { + if (!requestFit && controller?.hasGraph()) { + requestFit = true; + controller.getGraph().reset(); + controller.getGraph().layout(); + + // Fit padding after resize + setTimeout(() => { + controller.getGraph().fit(FIT_PADDING); + }, 0); + } + }, [controller]); + // // layoutEnd handling // @@ -656,124 +681,127 @@ const TopologyContent: React.FC<{ ) : ( - - - { - setEdgeMode(EdgeMode.ALL); + <> + + + + { + setEdgeMode(EdgeMode.ALL); + }, + disabled: EdgeMode.ALL === edgeMode, + icon: , + id: 'toolbar_edge_mode_all', + tooltip: 'Show all edges' }, - disabled: EdgeMode.ALL === edgeMode, - icon: , - id: 'toolbar_edge_mode_all', - tooltip: 'Show all edges' - }, - { - ariaLabel: 'Hide Healthy Edges', - callback: () => { - //change this back when we have the active styling - //setEdgeMode(EdgeMode.UNHEALTHY === edgeMode ? EdgeMode.ALL : EdgeMode.UNHEALTHY); - setEdgeMode(EdgeMode.UNHEALTHY); + { + ariaLabel: 'Hide Healthy Edges', + callback: () => { + //change this back when we have the active styling + //setEdgeMode(EdgeMode.UNHEALTHY === edgeMode ? EdgeMode.ALL : EdgeMode.UNHEALTHY); + setEdgeMode(EdgeMode.UNHEALTHY); + }, + disabled: EdgeMode.UNHEALTHY === edgeMode, + icon: , + id: 'toolbar_edge_mode_unhealthy', + tooltip: 'Hide healthy edges' }, - disabled: EdgeMode.UNHEALTHY === edgeMode, - icon: , - id: 'toolbar_edge_mode_unhealthy', - tooltip: 'Hide healthy edges' - }, - { - ariaLabel: 'Hide All Edges', - id: 'toolbar_edge_mode_none', - disabled: EdgeMode.NONE === edgeMode, - icon: , - tooltip: 'Hide all edges', - callback: () => { - //change this back when we have the active styling - //setEdgeMode(EdgeMode.NONE === edgeMode ? EdgeMode.ALL : EdgeMode.NONE); - setEdgeMode(EdgeMode.NONE); - } - }, - { - ariaLabel: 'Layout - Dagre', - id: 'toolbar_layout_dagre', - disabled: LayoutName.Dagre === layoutName, - icon: , - tooltip: 'Layout - dagre', - callback: () => { - setLayoutName(LayoutName.Dagre); + { + ariaLabel: 'Hide All Edges', + id: 'toolbar_edge_mode_none', + disabled: EdgeMode.NONE === edgeMode, + icon: , + tooltip: 'Hide all edges', + callback: () => { + //change this back when we have the active styling + //setEdgeMode(EdgeMode.NONE === edgeMode ? EdgeMode.ALL : EdgeMode.NONE); + setEdgeMode(EdgeMode.NONE); + } + }, + { + ariaLabel: 'Layout - Dagre', + id: 'toolbar_layout_dagre', + disabled: LayoutName.Dagre === layoutName, + icon: , + tooltip: 'Layout - dagre', + callback: () => { + setLayoutName(LayoutName.Dagre); + } + }, + { + ariaLabel: 'Layout - Grid', + id: 'toolbar_layout_grid', + disabled: LayoutName.Grid === layoutName, + icon: , + tooltip: 'Layout - grid', + callback: () => { + setLayoutName(LayoutName.Grid); + } + }, + { + ariaLabel: 'Layout - Concentric', + id: 'toolbar_layout_concentric', + disabled: LayoutName.Concentric === layoutName, + icon: , + tooltip: 'Layout - concentric', + callback: () => { + setLayoutName(LayoutName.Concentric); + } + }, + { + ariaLabel: 'Layout - Breadth First', + id: 'toolbar_layout_breadth_first', + disabled: LayoutName.BreadthFirst === layoutName, + icon: , + tooltip: 'Layout - breadth first', + callback: () => { + setLayoutName(LayoutName.BreadthFirst); + } } + ], + // currently unused + zoomInCallback: () => { + controller && controller.getGraph().scaleBy(ZOOM_IN); }, - { - ariaLabel: 'Layout - Grid', - id: 'toolbar_layout_grid', - disabled: LayoutName.Grid === layoutName, - icon: , - tooltip: 'Layout - grid', - callback: () => { - setLayoutName(LayoutName.Grid); - } + // currently unused + zoomOutCallback: () => { + controller && controller.getGraph().scaleBy(ZOOM_OUT); }, - { - ariaLabel: 'Layout - Concentric', - id: 'toolbar_layout_concentric', - disabled: LayoutName.Concentric === layoutName, - icon: , - tooltip: 'Layout - concentric', - callback: () => { - setLayoutName(LayoutName.Concentric); + resetViewCallback: () => { + if (controller) { + requestFit = true; + controller.getGraph().reset(); + controller.getGraph().layout(); } }, - { - ariaLabel: 'Layout - Breadth First', - id: 'toolbar_layout_breadth_first', - disabled: LayoutName.BreadthFirst === layoutName, - icon: , - tooltip: 'Layout - breadth first', - callback: () => { - setLayoutName(LayoutName.BreadthFirst); - } - } - ], - // currently unused - zoomInCallback: () => { - controller && controller.getGraph().scaleBy(ZOOM_IN); - }, - // currently unused - zoomOutCallback: () => { - controller && controller.getGraph().scaleBy(ZOOM_OUT); - }, - resetViewCallback: () => { - if (controller) { - requestFit = true; - controller.getGraph().reset(); - controller.getGraph().layout(); + legend: true, + legendIcon: , + legendTip: 'Legend', + legendCallback: () => { + if (toggleLegend) toggleLegend(); } - }, - legend: true, - legendIcon: , - legendTip: 'Legend', - legendCallback: () => { - if (toggleLegend) toggleLegend(); - } - })} - /> + })} + /> + - - } - > - - + } + > + + + ); }; From 464b51d7b68812e60e102e35c71f9d6b108fe7c0 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 30 Apr 2024 10:26:07 -0400 Subject: [PATCH 07/46] In anticipation of Fernando's resize fix, remove the unnecessary resize infra... --- frontend/src/pages/Mesh/Mesh.tsx | 11 +---------- frontend/src/pages/Mesh/MeshPage.tsx | 16 ++-------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/frontend/src/pages/Mesh/Mesh.tsx b/frontend/src/pages/Mesh/Mesh.tsx index ded790ce29..6ec3390038 100644 --- a/frontend/src/pages/Mesh/Mesh.tsx +++ b/frontend/src/pages/Mesh/Mesh.tsx @@ -97,7 +97,6 @@ const TopologyContent: React.FC<{ onEdgeTap?: (edge: Edge) => void; onNodeTap?: (node: Node) => void; onReady: (controller: any) => void; - onResize?: () => void; setLayout: (val: LayoutName) => void; setTarget: (meshTarget: MeshTarget) => void; setUpdateTime: (val: TimeInMilliseconds) => void; @@ -111,7 +110,6 @@ const TopologyContent: React.FC<{ onEdgeTap, onNodeTap, onReady, - onResize, setLayout: _setLayoutName, setTarget, setUpdateTime, @@ -196,11 +194,7 @@ const TopologyContent: React.FC<{ controller.getGraph().fit(FIT_PADDING); }, 0); } - - if (onResize) { - onResize(); - } - }, [onResize, controller]); + }, [controller]); // // layoutEnd handling @@ -575,7 +569,6 @@ export const Mesh: React.FC<{ onEdgeTap?: (edge: Edge) => void; onNodeTap?: (node: Node) => void; onReady: (controller: any) => void; - onResize: () => void; setLayout: (layout: Layout) => void; setTarget: (meshTarget: MeshTarget) => void; setUpdateTime: (val: TimeInMilliseconds) => void; @@ -587,7 +580,6 @@ export const Mesh: React.FC<{ onEdgeTap, onNodeTap, onReady, - onResize, setLayout, setTarget, setUpdateTime, @@ -643,7 +635,6 @@ export const Mesh: React.FC<{ layoutName={getLayoutName(layout)} onEdgeTap={onEdgeTap} onNodeTap={onNodeTap} - onResize={onResize} onReady={onReady} setLayout={setLayoutByName} setTarget={setTarget} diff --git a/frontend/src/pages/Mesh/MeshPage.tsx b/frontend/src/pages/Mesh/MeshPage.tsx index c452ecd833..ccfd6563b6 100644 --- a/frontend/src/pages/Mesh/MeshPage.tsx +++ b/frontend/src/pages/Mesh/MeshPage.tsx @@ -83,7 +83,6 @@ export type MeshData = { type MeshPageState = { meshData: MeshData; - lastResizeTime: TimeInMilliseconds; // just a way to force a top-down re-render on a mesh-level resize (e..f targetPanelCollapse) }; const containerStyle = kialiStyle({ @@ -149,8 +148,7 @@ class MeshPageComponent extends React.Component { isLoading: true, name: UNKNOWN, timestamp: 0 - }, - lastResizeTime: 0 + } }; } @@ -249,13 +247,7 @@ class MeshPageComponent extends React.Component { isLoading={this.state.meshData.isLoading} isMiniMesh={false} > - +
@@ -276,10 +268,6 @@ class MeshPageComponent extends React.Component { ); } - private onResize = () => { - this.setState({ lastResizeTime: Date.now() }); - }; - // TODO Focus... private onFocus = (focusNode: FocusNode) => { console.debug(`onFocus(${focusNode})`); From 96bad7897cb5a21de190e994ca5f176a53be16e5 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 30 Apr 2024 13:57:47 -0400 Subject: [PATCH 08/46] fix unit test --- frontend/src/pages/Overview/__tests__/OverviewSort.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts b/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts index b5012f8bb9..e39960a34a 100644 --- a/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts +++ b/frontend/src/pages/Overview/__tests__/OverviewSort.test.ts @@ -91,12 +91,12 @@ describe('Overview Page ', () => { it('sorts config asc', () => { const sortedNamespaces = sortFunc(allNamespaces, configSortField, true); expect(sortedNamespaces.map(n => n.name)).toEqual([ - 'istio-system', 'default', 'electronic-shop', 'alpha', 'beta', 'fraud-detection', + 'istio-system', 'travel-agency', 'travel-control', 'travel-portal' @@ -106,10 +106,10 @@ describe('Overview Page ', () => { it('sorts config desc', () => { const sortedNamespaces = sortFunc(allNamespaces, configSortField, false); expect(sortedNamespaces.map(n => n.name)).toEqual([ - 'istio-system', 'travel-portal', 'travel-control', 'travel-agency', + 'istio-system', 'fraud-detection', 'beta', 'alpha', From d3ca7b82f876db151505c77bcaaff9ba75fa63bc Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 30 Apr 2024 16:08:44 -0400 Subject: [PATCH 09/46] - finish removing use of '/api/clusters' API - note that '/api/mesh/ API is also only used in tests, but there is a decent amount of infra around that testing, and it does test the same core code as is used in the mesh graph gen, so I'm leaving it in place. --- mesh/api/api_test.go | 4 ++-- tests/integration/tests/clusters_test.go | 12 +++++------ tests/integration/utils/kiali/kiali_client.go | 21 ------------------- 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/mesh/api/api_test.go b/mesh/api/api_test.go index 5909fc13e7..b853bc55f7 100644 --- a/mesh/api/api_test.go +++ b/mesh/api/api_test.go @@ -247,7 +247,7 @@ func TestMeshGraph(t *testing.T) { var fut func(ctx context.Context, globalInfo *mesh.AppenderGlobalInfo, o mesh.Options) (int, interface{}) mr := mux.NewRouter() - mr.HandleFunc("/api/mesh", http.HandlerFunc( + mr.HandleFunc("/api/mesh/graph", http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { context := authentication.SetAuthInfoContext(r.Context(), &api.AuthInfo{Token: "test"}) code, config := fut(context, globalInfo, mesh.NewOptions(r.WithContext(context))) @@ -258,7 +258,7 @@ func TestMeshGraph(t *testing.T) { defer ts.Close() fut = graphMesh - url := ts.URL + "/api/mesh?queryTime=1523364075" + url := ts.URL + "/api/mesh/graph?queryTime=1523364075" resp, err := http.Get(url) if err != nil { t.Fatal(err) diff --git a/tests/integration/tests/clusters_test.go b/tests/integration/tests/clusters_test.go index 98996dcfb7..c438532db3 100644 --- a/tests/integration/tests/clusters_test.go +++ b/tests/integration/tests/clusters_test.go @@ -4,10 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" "github.com/kiali/kiali/config" - "github.com/kiali/kiali/kubernetes" "github.com/kiali/kiali/log" "github.com/kiali/kiali/tests/integration/utils/kiali" "github.com/kiali/kiali/tests/integration/utils/kube" @@ -17,8 +15,9 @@ func TestRemoteKialiShownInClustersResponse(t *testing.T) { require := require.New(t) // Get the number of clusters before we start. - originalClusters, err := kiali.Clusters() + kialiConfig, _, err := kiali.KialiConfig() require.NoError(err) + originalClusters := kialiConfig.Clusters ctx := contextWithTestingDeadline(t) dynamicClient := kube.NewDynamicClient(t) @@ -48,12 +47,13 @@ func TestRemoteKialiShownInClustersResponse(t *testing.T) { require.NoError(instance.UpdateConfig(ctx, conf)) require.NoError(instance.Restart(ctx)) - clusters, err := kiali.Clusters() + kialiConfig, _, err = kiali.KialiConfig() require.NoError(err) + clusters := kialiConfig.Clusters // Ensure the inaccessible cluster/kiali instance is shown in the clusters response. require.Greater(len(clusters), len(originalClusters)) - inaccessibleIdx := slices.IndexFunc(clusters, func(c kubernetes.Cluster) bool { return c.Name == "inaccessible" }) - require.NotEqualf(-1, inaccessibleIdx, "inaccessible cluster not found in clusters response") + inaccessible := clusters["inaccessible"] + require.NotNil(inaccessible, "inaccessible cluster not found in clusters response") } diff --git a/tests/integration/utils/kiali/kiali_client.go b/tests/integration/utils/kiali/kiali_client.go index f069dc4792..1ac55b1157 100644 --- a/tests/integration/utils/kiali/kiali_client.go +++ b/tests/integration/utils/kiali/kiali_client.go @@ -11,7 +11,6 @@ import ( "github.com/kiali/kiali/config" "github.com/kiali/kiali/graph/config/cytoscape" "github.com/kiali/kiali/handlers" - "github.com/kiali/kiali/kubernetes" "github.com/kiali/kiali/log" "github.com/kiali/kiali/models" "github.com/kiali/kiali/status" @@ -509,26 +508,6 @@ func Grafana() (*models.GrafanaInfo, int, error) { } } -func Clusters() ([]kubernetes.Cluster, error) { - url := fmt.Sprintf("%s/api/clusters", client.kialiURL) - body, code, _, err := httpGETWithRetry(url, client.GetAuth(), TIMEOUT, nil, nil) - if err != nil { - return nil, err - } - - if code != http.StatusOK { - return nil, fmt.Errorf("non 200 response code: %d when getting clusters. Body: %s", code, body) - } - - clusters := []kubernetes.Cluster{} - err = json.Unmarshal(body, &clusters) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal body into clusters. Body: %s", body) - } - - return clusters, nil -} - func getRequestAndUnmarshalInto[T any](url string, response *T) (int, error) { body, code, _, err := httpGETWithRetry(url, client.GetAuth(), TIMEOUT, nil, nil) if err != nil { From 0b18f1d82bf58941c1881d8d619804849e235a2e Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Wed, 1 May 2024 10:33:53 -0400 Subject: [PATCH 10/46] WIP - Cypress - expose PFT Controller in MeshPage state - note: not sure we can get state for Hooks, only classic classes, so using MeshPageComponent as opposed to MeshComponent. - start adding a few basic tests --- .../cypress/integration/common/mesh_test.ts | 96 +++++++++++++++++++ .../integration/featureFiles/mesh.feature | 29 ++++++ frontend/src/pages/Mesh/MeshPage.tsx | 22 +++-- 3 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 frontend/cypress/integration/common/mesh_test.ts create mode 100644 frontend/cypress/integration/featureFiles/mesh.feature diff --git a/frontend/cypress/integration/common/mesh_test.ts b/frontend/cypress/integration/common/mesh_test.ts new file mode 100644 index 0000000000..dcb9fd153d --- /dev/null +++ b/frontend/cypress/integration/common/mesh_test.ts @@ -0,0 +1,96 @@ +import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; +import { Controller, Edge, Node, isEdge, isNode } from '@patternfly/react-topology'; + +const url = '/console'; + +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 asks for mesh with refresh {string} and duration {string}', + (namespaces: string, refresh: string, duration: string) => { + cy.visit(`${url}/mesh?refresh=${refresh}&duration=${duration}&namespaces=${namespaces}`); + } +); + +When('user opens mesh tour', () => { + cy.get('button#mesh-tour').click(); +}); + +When('user closes mesh tour', () => { + cy.get('div[role="dialog"]').find('button[aria-label="Close"]').click(); +}); + +Then('user {string} mesh tour', (action: string) => { + if (action === 'sees') { + cy.get('div[role="dialog"]').find('span').contains('Shortcuts').should('exist'); + } else { + cy.get('div[role="dialog"]').should('not.exist'); + } +}); + +When('user clicks mesh duration menu', () => { + cy.get('button#time_range_duration-toggle').click(); +}); + +When(`user selects mesh duration {string}`, (duration: string) => { + cy.get('button#time_range_duration-toggle').click(); + cy.get(`button[id="${duration}"]`).click(); + cy.get('#loading_kiali_spinner').should('not.exist'); +}); + +When('user clicks mesh refresh menu', () => { + cy.get('button#time_range_refresh-toggle').click(); +}); + +When(`user selects mesh refresh {string}`, (refresh: string) => { + cy.get('button#time_range_refresh-toggle').click(); + cy.get(`button[id="${refresh}"]`).click().get('#loading_kiali_spinner').should('not.exist'); +}); + +Then('mesh side panel is shown', () => { + cy.get('#target-panel-mesh') + .should('be.visible') + .within(div => { + cy.contains('Mesh Name: Istio Mesh'); + }); +}); + +Then('user sees expected mesh infra', () => { + //cy.get('#loading_kiali_spinner').should('not.exist'); + cy.waitForReact(); + cy.getReact('MeshPageComponent') + .should('have.length', '2') + .nthNode(1) + .getCurrentState() + .then(state => { + const controller = state.controller; + assert.isTrue(controller.hasGraph()); + const { nodes, edges } = elems(controller); + assert.equal(nodes.length, 8, 'Unexpected number of infra nodes'); + assert.equal(edges.length, 5, 'Unexpected number of infra edges'); + }); +}); + +// Since I can't import from MeshElems.tsx, copying some helpers here... +const elems = (c: Controller): { edges: Edge[]; nodes: Node[] } => { + const elems = c.getElements(); + + return { + nodes: elems.filter(e => isNode(e)) as Node[], + edges: elems.filter(e => isEdge(e)) as Edge[] + }; +}; diff --git a/frontend/cypress/integration/featureFiles/mesh.feature b/frontend/cypress/integration/featureFiles/mesh.feature new file mode 100644 index 0000000000..b896430e8b --- /dev/null +++ b/frontend/cypress/integration/featureFiles/mesh.feature @@ -0,0 +1,29 @@ +@mesh-page +# don't change first line of this file - the tag is used for the test scripts to identify the test suite + +Feature: Kiali Mesh page + + User opens the Mesh page with bookinfo deployed + + Background: + Given user is at administrator perspective + And user is at the "mesh" page + +# NOTE: Mesh Find/Hide has its own feature file + + @selected + Scenario: Open mesh Tour + When user opens mesh tour + Then user "sees" mesh tour + + Scenario: Close mesh Tour + When user closes mesh tour + Then user "does not see" mesh tour + + @selected + Scenario: See mesh + Then mesh side panel is shown + And user sees expected mesh infra + + # @bookinfo-app + # Scenario: See DataPlane \ No newline at end of file diff --git a/frontend/src/pages/Mesh/MeshPage.tsx b/frontend/src/pages/Mesh/MeshPage.tsx index ccfd6563b6..81b18d5f2f 100644 --- a/frontend/src/pages/Mesh/MeshPage.tsx +++ b/frontend/src/pages/Mesh/MeshPage.tsx @@ -82,6 +82,7 @@ export type MeshData = { }; type MeshPageState = { + controller?: Controller; meshData: MeshData; }; @@ -127,20 +128,19 @@ const MeshErrorBoundaryFallback = () => { }; class MeshPageComponent extends React.Component { - private controller?: Controller; private readonly errorBoundaryRef: any; private focusNode?: FocusNode; private meshDataSource: MeshDataSource; constructor(props: MeshPageProps) { super(props); - this.controller = undefined; this.errorBoundaryRef = React.createRef(); const focusNodeId = getFocusSelector(); this.focusNode = focusNodeId ? { id: focusNodeId, isSelected: true } : undefined; this.meshDataSource = new MeshDataSource(); this.state = { + controller: undefined, meshData: { elements: { edges: [], nodes: [] }, elementsChanged: false, @@ -178,10 +178,6 @@ class MeshPageComponent extends React.Component { // settings. That in turn ensures the initial fetchParams are correct. const isInitialLoad = !this.state.meshData.timestamp; - if (curr.target?.type === 'mesh') { - this.controller = curr.target.elem as Controller; - } - if ( isInitialLoad || prev.duration !== curr.duration || @@ -218,7 +214,7 @@ class MeshPageComponent extends React.Component {
{ isLoading={this.state.meshData.isLoading} isMiniMesh={false} > - +
@@ -273,6 +275,10 @@ class MeshPageComponent extends React.Component { console.debug(`onFocus(${focusNode})`); }; + private handleReady = (controller: Controller) => { + this.setState({ controller: controller }); + }; + private handleEmptyMeshAction = () => { this.loadMeshFromBackend(); }; From e4215e6447282c1131ebffc48899852a0ab058ab Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Sun, 5 May 2024 11:45:25 -0400 Subject: [PATCH 11/46] some i18n updates --- frontend/public/locales/zh/translation.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json index cf1e00eb3d..78116de5e3 100644 --- a/frontend/public/locales/zh/translation.json +++ b/frontend/public/locales/zh/translation.json @@ -4,6 +4,7 @@ "{{count}} service_other": "{{count}} 服务", "{{count}} workload_other": "{{count}} 工作负载", "and": "and", + "API Endpoint": "API Endpoint", "Applications": "应用", "Apps": "应用", "Close Replay": "关闭重放", @@ -53,6 +54,7 @@ "Istio config": "Istio配置", "Istio Config": "Istio配置", "Istio deployment status disabled.": "Istio deployment status disabled.", + "Kiali home cluster": "Kiali home cluster", "Kiali home cluster: {{name}}": "Kiali主集群: {{name}}", "Kiali on GitHub": "Kiali on GitHub", "Last": "最近的", @@ -68,6 +70,7 @@ "Name": "名称", "Namespace": "命名空间", "Namespace Label": "命名空间标签", + "Network": "Network", "No": "没有", "No cert info": "没有集群", "No health information": "无正常信息", @@ -92,6 +95,7 @@ "Proxy push time": "代理推送时间", "Replay": "重放", "Replay...": "重放...", + "Secret Name": "Secret Name", "Services": "服务", "Subset": "子集", "Switch language": "Switch language", @@ -112,6 +116,7 @@ "Unreachable": "Unreachable", "Valid From:": "有效开始时间:", "Valid To:": "有效结束时间:", + "Version": "Version", "Visit the Mesh page": "Visit the Mesh page", "Workloads": "工作负载" } From da5d03666e4a3e6425027cef984f5891420900cc Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Sun, 5 May 2024 12:56:54 -0400 Subject: [PATCH 12/46] remove @selected from mesh feature --- frontend/cypress/integration/featureFiles/mesh.feature | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/cypress/integration/featureFiles/mesh.feature b/frontend/cypress/integration/featureFiles/mesh.feature index b896430e8b..37e9aeda8f 100644 --- a/frontend/cypress/integration/featureFiles/mesh.feature +++ b/frontend/cypress/integration/featureFiles/mesh.feature @@ -11,16 +11,15 @@ Feature: Kiali Mesh page # NOTE: Mesh Find/Hide has its own feature file - @selected Scenario: Open mesh Tour When user opens mesh tour Then user "sees" mesh tour Scenario: Close mesh Tour - When user closes mesh tour + When user opens mesh tour + And user closes mesh tour Then user "does not see" mesh tour - @selected Scenario: See mesh Then mesh side panel is shown And user sees expected mesh infra From b990914ea96b6a1d452abf35db3190b9c1931947 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Sun, 5 May 2024 14:46:47 -0400 Subject: [PATCH 13/46] Update some cypress tests to account for the removal of /api/clusters --- .../integration/common/sidecar_injection.ts | 41 ++++++++++++------- frontend/cypress/integration/common/table.ts | 19 +++++---- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/frontend/cypress/integration/common/sidecar_injection.ts b/frontend/cypress/integration/common/sidecar_injection.ts index c59d47b886..1df0d8422d 100644 --- a/frontend/cypress/integration/common/sidecar_injection.ts +++ b/frontend/cypress/integration/common/sidecar_injection.ts @@ -1,5 +1,6 @@ import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { ensureKialiFinishedLoading } from './transition'; +import { MeshCluster } from '../../../src/types/Mesh'; // Most of these "Given" implementations are directly using the Kiali API // in order to reach a well known state in the environment before performing @@ -247,11 +248,13 @@ When('I override the default automatic sidecar injection policy in the namespace cy.request('GET', '/api/status').then(response => { expect(response.status).to.equal(200); - cy.request('/api/clusters').then(response => { + cy.request('/api/config').then(response => { cy.wrap(response.isOkStatusCode).should('be.true'); - cy.wrap(response.body).should('have.length', 1); - const cluster = response.body[0].name; + const clusters: { [key: string]: MeshCluster } = response.body.clusters; + const clusterNames = Object.keys(clusters); + cy.wrap(clusterNames).should('have.length', 1); + const cluster = clusterNames[0]; cy.getBySel('overview-type-LIST').should('be.visible').click(); @@ -273,11 +276,13 @@ When( cy.request('GET', '/api/status').then(response => { expect(response.status).to.equal(200); - cy.request('/api/clusters').then(response => { + cy.request('/api/config').then(response => { cy.wrap(response.isOkStatusCode).should('be.true'); - cy.wrap(response.body).should('have.length', 1); - const cluster = response.body[0].name; + const clusters: { [key: string]: MeshCluster } = response.body.clusters; + const clusterNames = Object.keys(clusters); + cy.wrap(clusterNames).should('have.length', 1); + const cluster = clusterNames[0]; cy.getBySel('overview-type-LIST').should('be.visible').click(); @@ -300,11 +305,13 @@ When('I remove override configuration for sidecar injection in the namespace', f cy.request('GET', '/api/status').then(response => { expect(response.status).to.equal(200); - cy.request('/api/clusters').then(response => { + cy.request('/api/config').then(response => { cy.wrap(response.isOkStatusCode).should('be.true'); - cy.wrap(response.body).should('have.length', 1); - const cluster = response.body[0].name; + const clusters: { [key: string]: MeshCluster } = response.body.clusters; + const clusterNames = Object.keys(clusters); + cy.wrap(clusterNames).should('have.length', 1); + const cluster = clusterNames[0]; cy.getBySel('overview-type-LIST').should('be.visible').click(); @@ -351,11 +358,13 @@ Then('I should see the override annotation for sidecar injection in the namespac const expectation = 'exist'; - cy.request('/api/clusters').then(response => { + cy.request('/api/config').then(response => { cy.wrap(response.isOkStatusCode).should('be.true'); - cy.wrap(response.body).should('have.length', 1); - const cluster = response.body[0].name; + const clusters: { [key: string]: MeshCluster } = response.body.clusters; + const clusterNames = Object.keys(clusters); + cy.wrap(clusterNames).should('have.length', 1); + const cluster = clusterNames[0]; cy.getBySel(`VirtualItem_Cluster${cluster}_${this.targetNamespace}`) .contains(`istio-injection=${enabled}`) @@ -368,11 +377,13 @@ Then('I should see no override annotation for sidecar injection in the namespace cy.request('GET', '/api/status').then(response => { expect(response.status).to.equal(200); - cy.request('/api/clusters').then(response => { + cy.request('/api/config').then(response => { cy.wrap(response.isOkStatusCode).should('be.true'); - cy.wrap(response.body).should('have.length', 1); - const cluster = response.body[0].name; + const clusters: { [key: string]: MeshCluster } = response.body.clusters; + const clusterNames = Object.keys(clusters); + cy.wrap(clusterNames).should('have.length', 1); + const cluster = clusterNames[0]; cy.getBySel(`VirtualItem_Cluster${cluster}_${this.targetNamespace}`) .contains(`istio-injection`) diff --git a/frontend/cypress/integration/common/table.ts b/frontend/cypress/integration/common/table.ts index 81fafa4f07..e5179895ee 100644 --- a/frontend/cypress/integration/common/table.ts +++ b/frontend/cypress/integration/common/table.ts @@ -1,5 +1,6 @@ import { Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { TableDefinition } from 'cypress-cucumber-preprocessor'; +import { MeshCluster } from '../../../src/types/Mesh'; enum SortOrder { Ascending = 'ascending', @@ -210,15 +211,17 @@ export const checkHealthIndicatorInTable = ( : `${targetNamespace}_${targetRowItemName}`; // cy.getBySel(`VirtualItem_Ns${selector}]`).find('span').filter(`.icon-${healthStatus}`).should('exist'); - // Fetch the cluster info from /api/clusters + // Fetch the cluster info from /api/config // TODO: Move this somewhere else since other tests will most likely need this info as well. // VirtualItem_Clustercluster-default_Nsbookinfo_details // VirtualItem_Clustercluster-default_Nsbookinfo_productpage - cy.request('/api/clusters').then(response => { + cy.request('/api/config').then(response => { cy.wrap(response.isOkStatusCode).should('be.true'); - cy.wrap(response.body).should('have.length', 1); - const cluster = response.body[0].name; + const clusters: { [key: string]: MeshCluster } = response.body.clusters; + const clusterNames = Object.keys(clusters); + cy.wrap(clusterNames).should('have.length', 1); + const cluster = clusterNames[0]; cy.getBySel(`VirtualItem_Cluster${cluster}_Ns${selector}`) .find('span') @@ -237,11 +240,13 @@ export const checkHealthStatusInTable = ( ? `${targetNamespace}_${targetType}_${targetRowItemName}` : `${targetNamespace}_${targetRowItemName}`; - cy.request('/api/clusters').then(response => { + cy.request('/api/config').then(response => { cy.wrap(response.isOkStatusCode).should('be.true'); - cy.wrap(response.body).should('have.length', 1); - const cluster = response.body[0].name; + const clusters: { [key: string]: MeshCluster } = response.body.clusters; + const clusterNames = Object.keys(clusters); + cy.wrap(clusterNames).should('have.length', 1); + const cluster = clusterNames[0]; cy.get( `[data-test=VirtualItem_Cluster${cluster}_Ns${selector}] td:first-child span[class=pf-v5-c-icon__content]` From 5787fac990a0d26620e20359c45cdc7957c72bd8 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Mon, 6 May 2024 17:08:23 -0400 Subject: [PATCH 14/46] embed the data plane config in each expandable row --- frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx index cb2ac992e6..4bc0071301 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx @@ -86,6 +86,13 @@ export class TargetPanelDataPlane extends React.Component +
+                            {JSON.stringify(
+                              data.infraData.find(id => id.name === ns.name),
+                              null,
+                              2
+                            )}
+                          
@@ -93,7 +100,6 @@ export class TargetPanelDataPlane extends React.Component -
{JSON.stringify(data.infraData, null, 2)}
); From 26b147a97b88a9b0773289122c70b3cd4cd79d15 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Mon, 6 May 2024 17:08:40 -0400 Subject: [PATCH 15/46] If no health is set, assume healthy --- mesh/generator/generator.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mesh/generator/generator.go b/mesh/generator/generator.go index 67f59d553b..d65f763c7d 100644 --- a/mesh/generator/generator.go +++ b/mesh/generator/generator.go @@ -234,6 +234,8 @@ func addInfra(meshMap mesh.MeshMap, infraType, cluster, namespace, name string, if healthData != "" { node.Metadata[mesh.HealthData] = healthData + } else { + node.Metadata[mesh.HealthData] = kubernetes.ComponentHealthy } return node, found, nil From 9316a92f01b5f2c06e4c1e1acb2f237c1e0a6c3a Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Mon, 6 May 2024 17:09:45 -0400 Subject: [PATCH 16/46] WIP - add some test infra to allow the test code to select nodes... --- .../cypress/integration/common/mesh_test.ts | 58 ++++++++++++++++--- .../integration/featureFiles/mesh.feature | 7 +++ frontend/src/pages/Mesh/Mesh.tsx | 6 +- frontend/src/pages/Mesh/MeshPage.tsx | 5 +- 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/frontend/cypress/integration/common/mesh_test.ts b/frontend/cypress/integration/common/mesh_test.ts index dcb9fd153d..c933648c86 100644 --- a/frontend/cypress/integration/common/mesh_test.ts +++ b/frontend/cypress/integration/common/mesh_test.ts @@ -1,5 +1,6 @@ import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; -import { Controller, Edge, Node, isEdge, isNode } from '@patternfly/react-topology'; +import { Controller, Edge, Node, Visualization, isEdge, isNode } from '@patternfly/react-topology'; +import { MeshNodeData, MeshTarget } from '../../../src/types/Mesh'; const url = '/console'; @@ -19,12 +20,12 @@ Before(() => { }); }); -When( - 'user asks for mesh with refresh {string} and duration {string}', - (namespaces: string, refresh: string, duration: string) => { - cy.visit(`${url}/mesh?refresh=${refresh}&duration=${duration}&namespaces=${namespaces}`); - } -); +//When( +// 'user asks for mesh with refresh {string} and duration {string}', +// (namespaces: string, refresh: string, duration: string) => { +// cy.visit(`${url}/mesh?refresh=${refresh}&duration=${duration}&namespaces=${namespaces}`); +// } +//); When('user opens mesh tour', () => { cy.get('button#mesh-tour').click(); @@ -34,6 +35,28 @@ When('user closes mesh tour', () => { cy.get('div[role="dialog"]').find('button[aria-label="Close"]').click(); }); +When('user selects mesh node with label {string}', (label: string) => { + cy.get('#target-panel-mesh') + .should('be.visible') + .within(div => { + cy.contains('Mesh Name: Istio Mesh'); + }); + cy.waitForReact(); + cy.getReact('MeshPageComponent') + .should('have.length', '2') + .nthNode(1) + .getCurrentState() + .then(state => { + const controller = state.controller as Visualization; + assert.isTrue(controller.hasGraph()); + const { nodes } = elems(controller); + const node = nodes.find(n => n.getLabel() === label); + assert.exists(node); + const setSelectedIds = state.setSelectedIds as (values: string[]) => void; + setSelectedIds([node.getId()]); + }); +}); + Then('user {string} mesh tour', (action: string) => { if (action === 'sees') { cy.get('div[role="dialog"]').find('span').contains('Shortcuts').should('exist'); @@ -61,6 +84,11 @@ When(`user selects mesh refresh {string}`, (refresh: string) => { cy.get(`button[id="${refresh}"]`).click().get('#loading_kiali_spinner').should('not.exist'); }); +When(`user selects mesh refresh {string}`, (refresh: string) => { + cy.get('button#time_range_refresh-toggle').click(); + cy.get(`button[id="${refresh}"]`).click().get('#loading_kiali_spinner').should('not.exist'); +}); + Then('mesh side panel is shown', () => { cy.get('#target-panel-mesh') .should('be.visible') @@ -82,6 +110,22 @@ Then('user sees expected mesh infra', () => { const { nodes, edges } = elems(controller); assert.equal(nodes.length, 8, 'Unexpected number of infra nodes'); assert.equal(edges.length, 5, 'Unexpected number of infra edges'); + const nodeNames = nodes.map(n => n.getLabel()); + assert.isTrue(nodeNames.some(n => n === 'Data Plane')); + assert.isTrue(nodeNames.some(n => n === 'Grafana')); + assert.isTrue(nodeNames.some(n => n.startsWith('istiod'))); + assert.isTrue(nodeNames.some(n => n === 'jaeger' || n === 'Tempo')); + assert.isTrue(nodeNames.some(n => n === 'kiali')); + assert.isTrue(nodeNames.some(n => n === 'Prometheus')); + }); +}); + +Then('user sees data plane side panel', () => { + cy.get('#loading_kiali_spinner').should('not.exist'); + cy.get('#target-panel-mesh') + .should('be.visible') + .within(div => { + cy.contains('Data Plane'); }); }); diff --git a/frontend/cypress/integration/featureFiles/mesh.feature b/frontend/cypress/integration/featureFiles/mesh.feature index 37e9aeda8f..36f9de2c68 100644 --- a/frontend/cypress/integration/featureFiles/mesh.feature +++ b/frontend/cypress/integration/featureFiles/mesh.feature @@ -11,10 +11,12 @@ Feature: Kiali Mesh page # NOTE: Mesh Find/Hide has its own feature file + @selected Scenario: Open mesh Tour When user opens mesh tour Then user "sees" mesh tour + @selected Scenario: Close mesh Tour When user opens mesh tour And user closes mesh tour @@ -24,5 +26,10 @@ Feature: Kiali Mesh page Then mesh side panel is shown And user sees expected mesh infra + @selected + Scenario: Test DataPlane + When user selects mesh node with label "Data Plane" + Then user sees data plane side panel + # @bookinfo-app # Scenario: See DataPlane \ No newline at end of file diff --git a/frontend/src/pages/Mesh/Mesh.tsx b/frontend/src/pages/Mesh/Mesh.tsx index 6ec3390038..9af38ac2ae 100644 --- a/frontend/src/pages/Mesh/Mesh.tsx +++ b/frontend/src/pages/Mesh/Mesh.tsx @@ -96,7 +96,7 @@ const TopologyContent: React.FC<{ meshData: MeshData; onEdgeTap?: (edge: Edge) => void; onNodeTap?: (node: Node) => void; - onReady: (controller: any) => void; + onReady: (controller: any, setSelectedIds: (value: string[]) => void) => void; setLayout: (val: LayoutName) => void; setTarget: (meshTarget: MeshTarget) => void; setUpdateTime: (val: TimeInMilliseconds) => void; @@ -411,7 +411,7 @@ const TopologyContent: React.FC<{ if (initialGraph) { console.debug('mesh onReady'); - onReady(controller); + onReady(controller, setSelectedIds); } // notify that the graph has been updated @@ -568,7 +568,7 @@ export const Mesh: React.FC<{ meshData: MeshData; onEdgeTap?: (edge: Edge) => void; onNodeTap?: (node: Node) => void; - onReady: (controller: any) => void; + onReady: (controller: any, setSelectedIds: (values: string[]) => void) => void; setLayout: (layout: Layout) => void; setTarget: (meshTarget: MeshTarget) => void; setUpdateTime: (val: TimeInMilliseconds) => void; diff --git a/frontend/src/pages/Mesh/MeshPage.tsx b/frontend/src/pages/Mesh/MeshPage.tsx index 81b18d5f2f..0d3b696133 100644 --- a/frontend/src/pages/Mesh/MeshPage.tsx +++ b/frontend/src/pages/Mesh/MeshPage.tsx @@ -84,6 +84,7 @@ export type MeshData = { type MeshPageState = { controller?: Controller; meshData: MeshData; + setSelectedIds?: (values: string[]) => void; }; const containerStyle = kialiStyle({ @@ -275,8 +276,8 @@ class MeshPageComponent extends React.Component { console.debug(`onFocus(${focusNode})`); }; - private handleReady = (controller: Controller) => { - this.setState({ controller: controller }); + private handleReady = (controller: Controller, setSelectedIds: (values: string[]) => void) => { + this.setState({ controller: controller, setSelectedIds: setSelectedIds }); }; private handleEmptyMeshAction = () => { From b539788c1d81e5ba9346de23deeee1dc86fa34c9 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 7 May 2024 09:14:36 -0400 Subject: [PATCH 17/46] Update expected mesh graph. --- mesh/api/testdata/test_mesh_graph.expected | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected index c4866220c0..8470765590 100644 --- a/mesh/api/testdata/test_mesh_graph.expected +++ b/mesh/api/testdata/test_mesh_graph.expected @@ -9,7 +9,7 @@ "infraType": "cluster", "namespace": "", "nodeType": "box", - "healthData": null, + "healthData": "Healthy", "infraData": { "apiEndpoint": "http://127.0.0.2:9443", "isKialiHome": true, @@ -38,7 +38,7 @@ "infraType": "cluster", "namespace": "", "nodeType": "box", - "healthData": null, + "healthData": "Healthy", "isBox": "cluster", "isExternal": true, "isInaccessible": true @@ -66,7 +66,7 @@ "infraType": "dataplane", "namespace": "", "nodeType": "infra", - "healthData": null, + "healthData": "Healthy", "infraData": [ { "name": "data-plane-1", @@ -121,7 +121,7 @@ "infraType": "kiali", "namespace": "istio-system", "nodeType": "infra", - "healthData": null, + "healthData": "Healthy", "infraData": { "ComponentStatuses": { "Enabled": true, From 619292315b67c591bdf5ddf31351595fb1f3621a Mon Sep 17 00:00:00 2001 From: Fernando Hoyos Date: Tue, 7 May 2024 17:34:36 +0200 Subject: [PATCH 18/46] Add managed clusters to infra mesh graph --- mesh/generator/generator.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mesh/generator/generator.go b/mesh/generator/generator.go index d65f763c7d..feeefb79db 100644 --- a/mesh/generator/generator.go +++ b/mesh/generator/generator.go @@ -73,13 +73,15 @@ func BuildMeshMap(ctx context.Context, o mesh.Options, gi *mesh.AppenderGlobalIn if _, ok := clusterMap[cp.Cluster.Name]; !ok { _, _, err := addInfra(meshMap, mesh.InfraTypeCluster, cp.Cluster.Name, "", cp.Cluster.Name, cp.Cluster, esVersions[cp.Cluster.Name], false, "") mesh.CheckError(err) + clusterMap[cp.Cluster.Name] = true } // add managed clusters if not already added for _, mc := range cp.ManagedClusters { - if _, ok := clusterMap[mc.Name]; ok { + if _, ok := clusterMap[mc.Name]; !ok { _, _, err := addInfra(meshMap, mesh.InfraTypeCluster, mc.Name, "", mc.Name, mc, "", false, "") mesh.CheckError(err) + clusterMap[mc.Name] = true continue } From a174c9ba504c0773e954d9cfa8b342cdd07c1d63 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 7 May 2024 13:25:13 -0400 Subject: [PATCH 19/46] Enhance to test to cover multi-cluster primary-remote - note that the expected result is actually wrong due to a bug, will be fixed shortly via Fernando's PR, and the expected result will be updated. --- mesh/api/api_test.go | 53 ++-- mesh/api/testdata/test_mesh_graph.expected | 320 ++++++++++++--------- 2 files changed, 211 insertions(+), 162 deletions(-) diff --git a/mesh/api/api_test.go b/mesh/api/api_test.go index b853bc55f7..daf8935d7f 100644 --- a/mesh/api/api_test.go +++ b/mesh/api/api_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "os" "runtime" + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -19,7 +20,6 @@ import ( apps_v1 "k8s.io/api/apps/v1" core_v1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - api_runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd/api" @@ -27,6 +27,7 @@ import ( "github.com/kiali/kiali/business/authentication" "github.com/kiali/kiali/config" "github.com/kiali/kiali/kubernetes" + "github.com/kiali/kiali/kubernetes/cache" "github.com/kiali/kiali/kubernetes/kubetest" "github.com/kiali/kiali/mesh" "github.com/kiali/kiali/status" @@ -53,7 +54,7 @@ func setupMocks(t *testing.T) *mesh.AppenderGlobalInfo { Env: []core_v1.EnvVar{ { Name: "CLUSTER_ID", - Value: "East", + Value: "cluster-primary", }, { Name: "PILOT_SCOPE_GATEWAY_TO_NAMESPACE", @@ -98,7 +99,14 @@ trustDomain: cluster.local }, } - objects := []api_runtime.Object{ + assert := assert.New(t) + require := require.New(t) + conf := config.NewConfig() + conf.InCluster = false + conf.KubernetesConfig.ClusterName = "cluster-primary" + config.Set(conf) + + primaryClient := kubetest.NewFakeK8sClient( &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "istio-system"}}, &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "data-plane-1"}}, &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "data-plane-2"}}, @@ -106,35 +114,42 @@ trustDomain: cluster.local &istioConfigMap, &sidecarConfigMap, &kialiSvc, - } - - assert := assert.New(t) - require := require.New(t) - conf := config.NewConfig() - conf.InCluster = false - conf.KubernetesConfig.ClusterName = "East" - config.Set(conf) - - k8s := kubetest.NewFakeK8sClient(objects...) - k8s.KubeClusterInfo = kubernetes.ClusterInfo{ + ) + primaryClient.KubeClusterInfo = kubernetes.ClusterInfo{ ClientConfig: &rest.Config{ Host: "http://127.0.0.2:9443", }, } + remoteClient := kubetest.NewFakeK8sClient( + &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "data-plane-3"}}, + &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "data-plane-4"}}, + ) + clients := map[string]kubernetes.ClientInterface{ + "cluster-primary": primaryClient, + "cluster-remote": remoteClient, + } + + mockClientFactory := kubetest.NewK8SClientFactoryMock(nil) + mockClientFactory.SetClients(clients) + + cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf) - //mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) - business.SetupBusinessLayer(t, k8s, *conf) - clients := map[string]kubernetes.ClientInterface{conf.KubernetesConfig.ClusterName: k8s} + business.WithKialiCache(cache) + business.SetWithBackends(mockClientFactory, nil) layer := business.NewWithBackends(clients, clients, nil, nil) meshSvc := layer.Mesh //r := httptest.NewRequest("GET", "http://kiali.url.local/", nil) a, err := meshSvc.GetClusters() + sort.Slice(a, func(i, j int) bool { + return a[i].Name < a[j].Name + }) require.Nil(err, "GetClusters returned error: %v", err) require.NotNil(a, "GetClusters returned nil") - require.Len(a, 1, "GetClusters didn't resolve the Kiali cluster") - assert.Equal("East", a[0].Name, "Unexpected cluster name") + require.Len(a, 2, "GetClusters didn't resolve the Primnary and Remote clusters") + assert.Equal("cluster-primary", a[0].Name, "Unexpected primary cluster name") + assert.Equal("cluster-remote", a[1].Name, "Unexpected remote cluster name") assert.True(a[0].IsKialiHome, "Kiali cluster not properly marked as such") assert.Equal("http://127.0.0.2:9443", a[0].ApiEndpoint) assert.Equal("kialiNetwork", a[0].Network) diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected index 8470765590..751763cbae 100644 --- a/mesh/api/testdata/test_mesh_graph.expected +++ b/mesh/api/testdata/test_mesh_graph.expected @@ -3,9 +3,23 @@ "nodes": [ { "data": { - "id": "2411347cfd042289a2ccec31ae4de661", - "cluster": "East", - "infraName": "East", + "id": "f2a39c06544bab3ee6f6b9014db8530d", + "cluster": "_external_", + "infraName": "External Deployments", + "infraType": "cluster", + "namespace": "", + "nodeType": "box", + "healthData": "Healthy", + "isBox": "cluster", + "isExternal": true, + "isInaccessible": true + } + }, + { + "data": { + "id": "8b0417cc2584b04a88544ddcb1c18174", + "cluster": "cluster-primary", + "infraName": "cluster-primary", "infraType": "cluster", "namespace": "", "nodeType": "box", @@ -22,7 +36,7 @@ "version": "" } ], - "name": "East", + "name": "cluster-primary", "network": "kialiNetwork", "secretName": "", "accessible": true @@ -32,36 +46,132 @@ }, { "data": { - "id": "f2a39c06544bab3ee6f6b9014db8530d", + "id": "54de7d83a35cb05d3834c1b53d7c53e6", + "parent": "8b0417cc2584b04a88544ddcb1c18174", + "cluster": "cluster-primary", + "infraName": "istio-system", + "infraType": "namespace", + "namespace": "istio-system", + "nodeType": "box", + "healthData": null, + "isBox": "namespace" + } + }, + { + "data": { + "id": "0ff3498362503e275d0d47f0e6ca5479", + "parent": "f2a39c06544bab3ee6f6b9014db8530d", "cluster": "_external_", - "infraName": "External Deployments", - "infraType": "cluster", + "infraName": "Prometheus", + "infraType": "metricStore", "namespace": "", - "nodeType": "box", + "nodeType": "infra", "healthData": "Healthy", - "isBox": "cluster", + "infraData": { + "Auth": { + "CAFile": "xxx", + "InsecureSkipVerify": false, + "Password": "xxx", + "Token": "xxx", + "Type": "none", + "UseKialiToken": false, + "Username": "xxx" + }, + "CacheDuration": 7, + "CacheEnabled": true, + "CacheExpiration": 300, + "CustomHeaders": {}, + "HealthCheckUrl": "", + "IsCore": false, + "QueryScope": {}, + "ThanosProxy": { + "Enabled": false, + "RetentionPeriod": "7d", + "ScrapeInterval": "30s" + }, + "URL": "http://prometheus.istio-system:9090" + }, "isExternal": true, "isInaccessible": true } }, { "data": { - "id": "a7784c32ca8454299b1c0d174df2034d", - "parent": "2411347cfd042289a2ccec31ae4de661", - "cluster": "East", - "infraName": "istio-system", - "infraType": "namespace", - "namespace": "istio-system", - "nodeType": "box", - "healthData": null, - "isBox": "namespace" + "id": "726a5e87be54c6dfbbc55b22e3cbb1c5", + "parent": "f2a39c06544bab3ee6f6b9014db8530d", + "cluster": "_external_", + "infraName": "Grafana", + "infraType": "grafana", + "namespace": "", + "nodeType": "infra", + "healthData": "Healthy", + "infraData": { + "Auth": { + "CAFile": "xxx", + "InsecureSkipVerify": false, + "Password": "xxx", + "Token": "xxx", + "Type": "none", + "UseKialiToken": false, + "Username": "xxx" + }, + "Dashboards": null, + "Enabled": true, + "HealthCheckUrl": "", + "InClusterURL": "http://grafana.istio-system:3000", + "IsCore": false, + "URL": "" + }, + "isExternal": true, + "isInaccessible": true } }, { "data": { - "id": "3ab2df5f89394de1661e4b230d8fd488", - "parent": "2411347cfd042289a2ccec31ae4de661", - "cluster": "East", + "id": "e4dddd6aa55b9806c0e99646d0ac4711", + "parent": "f2a39c06544bab3ee6f6b9014db8530d", + "cluster": "_external_", + "infraName": "jaeger", + "infraType": "traceStore", + "namespace": "", + "nodeType": "infra", + "healthData": "Healthy", + "infraData": { + "Auth": { + "CAFile": "xxx", + "InsecureSkipVerify": false, + "Password": "xxx", + "Token": "xxx", + "Type": "none", + "UseKialiToken": false, + "Username": "xxx" + }, + "Enabled": true, + "HealthCheckUrl": "", + "GrpcPort": 9095, + "InClusterURL": "http://tracing.istio-system:16685/jaeger", + "IsCore": false, + "Provider": "jaeger", + "TempoConfig": {}, + "NamespaceSelector": true, + "QueryScope": {}, + "QueryTimeout": 5, + "URL": "", + "UseGRPC": true, + "WhiteListIstioSystem": [ + "jaeger-query", + "istio-ingressgateway" + ] + }, + "isExternal": true, + "isInaccessible": true + } + }, + { + "data": { + "id": "7c9269b87249746045770ea45dec2786", + "parent": "8b0417cc2584b04a88544ddcb1c18174", + "cluster": "cluster-primary", "infraName": "Data Plane", "infraType": "dataplane", "namespace": "", @@ -70,14 +180,14 @@ "infraData": [ { "name": "data-plane-1", - "cluster": "East", + "cluster": "cluster-primary", "isAmbient": false, "labels": null, "annotations": null }, { "name": "data-plane-2", - "cluster": "East", + "cluster": "cluster-primary", "isAmbient": false, "labels": null, "annotations": null @@ -87,9 +197,9 @@ }, { "data": { - "id": "46d95dbc7d76eaba80100dfa480dea56", - "parent": "a7784c32ca8454299b1c0d174df2034d", - "cluster": "East", + "id": "b4fef251c5e835d9269751857825b5ea", + "parent": "54de7d83a35cb05d3834c1b53d7c53e6", + "cluster": "cluster-primary", "infraName": "istiod", "infraType": "istiod", "namespace": "istio-system", @@ -114,9 +224,9 @@ }, { "data": { - "id": "7b74d3459937f2d07ade2827bc6d3aac", - "parent": "a7784c32ca8454299b1c0d174df2034d", - "cluster": "East", + "id": "ed31cec8e279d017c1ac1f7100723e79", + "parent": "54de7d83a35cb05d3834c1b53d7c53e6", + "cluster": "cluster-primary", "infraName": "kiali", "infraType": "kiali", "namespace": "istio-system", @@ -172,148 +282,72 @@ }, { "data": { - "id": "0ff3498362503e275d0d47f0e6ca5479", - "parent": "f2a39c06544bab3ee6f6b9014db8530d", - "cluster": "_external_", - "infraName": "Prometheus", - "infraType": "metricStore", + "id": "f50226d64063fb859a1d7fad6a609978", + "cluster": "cluster-remote", + "infraName": "Data Plane", + "infraType": "dataplane", "namespace": "", "nodeType": "infra", "healthData": "Healthy", - "infraData": { - "Auth": { - "CAFile": "xxx", - "InsecureSkipVerify": false, - "Password": "xxx", - "Token": "xxx", - "Type": "none", - "UseKialiToken": false, - "Username": "xxx" - }, - "CacheDuration": 7, - "CacheEnabled": true, - "CacheExpiration": 300, - "CustomHeaders": {}, - "HealthCheckUrl": "", - "IsCore": false, - "QueryScope": {}, - "ThanosProxy": { - "Enabled": false, - "RetentionPeriod": "7d", - "ScrapeInterval": "30s" + "infraData": [ + { + "name": "data-plane-3", + "cluster": "cluster-remote", + "isAmbient": false, + "labels": null, + "annotations": null }, - "URL": "http://prometheus.istio-system:9090" - }, - "isExternal": true, - "isInaccessible": true + { + "name": "data-plane-4", + "cluster": "cluster-remote", + "isAmbient": false, + "labels": null, + "annotations": null + } + ] } - }, + } + ], + "edges": [ { "data": { - "id": "726a5e87be54c6dfbbc55b22e3cbb1c5", - "parent": "f2a39c06544bab3ee6f6b9014db8530d", - "cluster": "_external_", - "infraName": "Grafana", - "infraType": "grafana", - "namespace": "", - "nodeType": "infra", - "healthData": "Healthy", - "infraData": { - "Auth": { - "CAFile": "xxx", - "InsecureSkipVerify": false, - "Password": "xxx", - "Token": "xxx", - "Type": "none", - "UseKialiToken": false, - "Username": "xxx" - }, - "Dashboards": null, - "Enabled": true, - "HealthCheckUrl": "", - "InClusterURL": "http://grafana.istio-system:3000", - "IsCore": false, - "URL": "" - }, - "isExternal": true, - "isInaccessible": true + "id": "c33dfb69aa02feb7e0798c9185a68525", + "source": "b4fef251c5e835d9269751857825b5ea", + "target": "7c9269b87249746045770ea45dec2786" } }, { "data": { - "id": "e4dddd6aa55b9806c0e99646d0ac4711", - "parent": "f2a39c06544bab3ee6f6b9014db8530d", - "cluster": "_external_", - "infraName": "jaeger", - "infraType": "traceStore", - "namespace": "", - "nodeType": "infra", - "healthData": "Healthy", - "infraData": { - "Auth": { - "CAFile": "xxx", - "InsecureSkipVerify": false, - "Password": "xxx", - "Token": "xxx", - "Type": "none", - "UseKialiToken": false, - "Username": "xxx" - }, - "Enabled": true, - "HealthCheckUrl": "", - "GrpcPort": 9095, - "InClusterURL": "http://tracing.istio-system:16685/jaeger", - "IsCore": false, - "Provider": "jaeger", - "TempoConfig": {}, - "NamespaceSelector": true, - "QueryScope": {}, - "QueryTimeout": 5, - "URL": "", - "UseGRPC": true, - "WhiteListIstioSystem": [ - "jaeger-query", - "istio-ingressgateway" - ] - }, - "isExternal": true, - "isInaccessible": true - } - } - ], - "edges": [ - { - "data": { - "id": "42fc58fc1ba10ba8b90db16dfde683e5", - "source": "46d95dbc7d76eaba80100dfa480dea56", - "target": "3ab2df5f89394de1661e4b230d8fd488" + "id": "758f05601a173df0bed8693eaf5cfbb6", + "source": "b4fef251c5e835d9269751857825b5ea", + "target": "f50226d64063fb859a1d7fad6a609978" } }, { "data": { - "id": "ca6610898d355f2112b9f0fe60d38ded", - "source": "7b74d3459937f2d07ade2827bc6d3aac", + "id": "574ebfd667eaa055aeefdec6d46cd012", + "source": "ed31cec8e279d017c1ac1f7100723e79", "target": "0ff3498362503e275d0d47f0e6ca5479" } }, { "data": { - "id": "e9224ceaa56b7b51afa4cb8cf4f141bf", - "source": "7b74d3459937f2d07ade2827bc6d3aac", - "target": "46d95dbc7d76eaba80100dfa480dea56" + "id": "4f46af89fe1178d9847cec0dde5ea9da", + "source": "ed31cec8e279d017c1ac1f7100723e79", + "target": "726a5e87be54c6dfbbc55b22e3cbb1c5" } }, { "data": { - "id": "1a9d17c36fef9b15660c16a58c0a79f2", - "source": "7b74d3459937f2d07ade2827bc6d3aac", - "target": "726a5e87be54c6dfbbc55b22e3cbb1c5" + "id": "b1314f35029eaa664b52b865540e1427", + "source": "ed31cec8e279d017c1ac1f7100723e79", + "target": "b4fef251c5e835d9269751857825b5ea" } }, { "data": { - "id": "32a914d7695d6d0e86b3629d58d0ce07", - "source": "7b74d3459937f2d07ade2827bc6d3aac", + "id": "7410fb5a927072cb1547d44b61e104ec", + "source": "ed31cec8e279d017c1ac1f7100723e79", "target": "e4dddd6aa55b9806c0e99646d0ac4711" } } From f7d5ef83d7061c0f563d7c06d208a99527928dd3 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 7 May 2024 15:11:49 -0400 Subject: [PATCH 20/46] update mesh generator test to be multi-cluster --- mesh/api/api_test.go | 43 ++++++++++++---------- mesh/api/testdata/test_mesh_graph.expected | 22 +++++++++++ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/mesh/api/api_test.go b/mesh/api/api_test.go index daf8935d7f..353dfe59d3 100644 --- a/mesh/api/api_test.go +++ b/mesh/api/api_test.go @@ -35,6 +35,12 @@ import ( // Setup mock func setupMocks(t *testing.T) *mesh.AppenderGlobalInfo { + assert := assert.New(t) + require := require.New(t) + conf := config.NewConfig() + conf.KubernetesConfig.ClusterName = "cluster-primary" + config.Set(conf) + istiodDeployment := apps_v1.Deployment{ ObjectMeta: v1.ObjectMeta{ Name: "istiod", @@ -43,21 +49,17 @@ func setupMocks(t *testing.T) *mesh.AppenderGlobalInfo { }, Spec: apps_v1.DeploymentSpec{ Template: core_v1.PodTemplateSpec{ - ObjectMeta: v1.ObjectMeta{ - Name: "istiod", - Namespace: "istio-system", - Labels: map[string]string{"app": "istiod"}, - }, Spec: core_v1.PodSpec{ Containers: []core_v1.Container{ { + Name: "discovery", Env: []core_v1.EnvVar{ { Name: "CLUSTER_ID", - Value: "cluster-primary", + Value: conf.KubernetesConfig.ClusterName, }, - { - Name: "PILOT_SCOPE_GATEWAY_TO_NAMESPACE", + core_v1.EnvVar{ + Name: "EXTERNAL_ISTIOD", Value: "true", }, }, @@ -99,13 +101,6 @@ trustDomain: cluster.local }, } - assert := assert.New(t) - require := require.New(t) - conf := config.NewConfig() - conf.InCluster = false - conf.KubernetesConfig.ClusterName = "cluster-primary" - config.Set(conf) - primaryClient := kubetest.NewFakeK8sClient( &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "istio-system"}}, &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "data-plane-1"}}, @@ -121,25 +116,33 @@ trustDomain: cluster.local }, } remoteClient := kubetest.NewFakeK8sClient( + &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{ + Name: "istio-system", + Annotations: map[string]string{business.IstioControlPlaneClustersLabel: conf.KubernetesConfig.ClusterName}, + }}, &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "data-plane-3"}}, &core_v1.Namespace{ObjectMeta: v1.ObjectMeta{Name: "data-plane-4"}}, ) clients := map[string]kubernetes.ClientInterface{ - "cluster-primary": primaryClient, - "cluster-remote": remoteClient, + conf.KubernetesConfig.ClusterName: primaryClient, + "cluster-remote": remoteClient, } + factory := kubetest.NewK8SClientFactoryMock(nil) + factory.SetClients(clients) mockClientFactory := kubetest.NewK8SClientFactoryMock(nil) mockClientFactory.SetClients(clients) - cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf) - business.WithKialiCache(cache) business.SetWithBackends(mockClientFactory, nil) layer := business.NewWithBackends(clients, clients, nil, nil) meshSvc := layer.Mesh - //r := httptest.NewRequest("GET", "http://kiali.url.local/", nil) + meshDef, err := meshSvc.GetMesh(context.TODO()) + require.NoError(err) + require.Len(meshDef.ControlPlanes, 1) + require.Len(meshDef.ControlPlanes[0].ManagedClusters, 2) + a, err := meshSvc.GetClusters() sort.Slice(a, func(i, j int) bool { return a[i].Name < a[j].Name diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected index 751763cbae..339e5d7532 100644 --- a/mesh/api/testdata/test_mesh_graph.expected +++ b/mesh/api/testdata/test_mesh_graph.expected @@ -44,6 +44,27 @@ "isBox": "cluster" } }, + { + "data": { + "id": "ecbd12dbd745b071361a4fbb12f3ebe8", + "cluster": "cluster-remote", + "infraName": "cluster-remote", + "infraType": "cluster", + "namespace": "", + "nodeType": "box", + "healthData": "Healthy", + "infraData": { + "apiEndpoint": "", + "isKialiHome": false, + "kialiInstances": null, + "name": "cluster-remote", + "network": "", + "secretName": "", + "accessible": true + }, + "isBox": "cluster" + } + }, { "data": { "id": "54de7d83a35cb05d3834c1b53d7c53e6", @@ -283,6 +304,7 @@ { "data": { "id": "f50226d64063fb859a1d7fad6a609978", + "parent": "ecbd12dbd745b071361a4fbb12f3ebe8", "cluster": "cluster-remote", "infraName": "Data Plane", "infraType": "dataplane", From 8817a3d8850a035dd66fe2128a4ec852dc3b73f8 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Tue, 7 May 2024 20:31:21 -0400 Subject: [PATCH 21/46] - Call out the MeshRefs we pass back up for use in toolbar, tests, etc - More basic cypress tests --- .../cypress/integration/common/mesh_test.ts | 46 +++++++++++++++++-- .../integration/featureFiles/mesh.feature | 26 ++++++++++- frontend/src/pages/Mesh/Mesh.tsx | 8 ++-- frontend/src/pages/Mesh/MeshPage.tsx | 17 ++++--- .../Mesh/target/TargetPanelControlPlane.tsx | 2 +- .../Mesh/target/TargetPanelDataPlane.tsx | 2 +- .../Mesh/target/TargetPanelNamespace.tsx | 2 +- .../src/pages/Mesh/target/TargetPanelNode.tsx | 2 +- 8 files changed, 85 insertions(+), 20 deletions(-) diff --git a/frontend/cypress/integration/common/mesh_test.ts b/frontend/cypress/integration/common/mesh_test.ts index c933648c86..fccc9e67db 100644 --- a/frontend/cypress/integration/common/mesh_test.ts +++ b/frontend/cypress/integration/common/mesh_test.ts @@ -47,12 +47,12 @@ When('user selects mesh node with label {string}', (label: string) => { .nthNode(1) .getCurrentState() .then(state => { - const controller = state.controller as Visualization; + const controller = state.meshRefs.controller as Visualization; assert.isTrue(controller.hasGraph()); const { nodes } = elems(controller); const node = nodes.find(n => n.getLabel() === label); assert.exists(node); - const setSelectedIds = state.setSelectedIds as (values: string[]) => void; + const setSelectedIds = state.meshRefs.setSelectedIds as (values: string[]) => void; setSelectedIds([node.getId()]); }); }); @@ -90,6 +90,7 @@ When(`user selects mesh refresh {string}`, (refresh: string) => { }); Then('mesh side panel is shown', () => { + cy.get('#loading_kiali_spinner').should('not.exist'); cy.get('#target-panel-mesh') .should('be.visible') .within(div => { @@ -98,14 +99,13 @@ Then('mesh side panel is shown', () => { }); Then('user sees expected mesh infra', () => { - //cy.get('#loading_kiali_spinner').should('not.exist'); cy.waitForReact(); cy.getReact('MeshPageComponent') .should('have.length', '2') .nthNode(1) .getCurrentState() .then(state => { - const controller = state.controller; + const controller = state.meshRefs.controller; assert.isTrue(controller.hasGraph()); const { nodes, edges } = elems(controller); assert.equal(nodes.length, 8, 'Unexpected number of infra nodes'); @@ -120,15 +120,51 @@ Then('user sees expected mesh infra', () => { }); }); +Then('user sees {string} cluster side panel', (name: string) => { + cy.get('#loading_kiali_spinner').should('not.exist'); + cy.get('#target-panel-cluster') + .should('be.visible') + .within(div => { + cy.contains(name); + }); +}); + +Then('user sees control plane side panel', () => { + cy.get('#loading_kiali_spinner').should('not.exist'); + cy.get('#target-panel-control-plane') + .should('be.visible') + .within(div => { + cy.contains('istiod-default'); + }); +}); + Then('user sees data plane side panel', () => { cy.get('#loading_kiali_spinner').should('not.exist'); - cy.get('#target-panel-mesh') + cy.get('#target-panel-data-plane') .should('be.visible') .within(div => { cy.contains('Data Plane'); }); }); +Then('user sees {string} namespace side panel', (name: string) => { + cy.get('#loading_kiali_spinner').should('not.exist'); + cy.get('#target-panel-namespace') + .should('be.visible') + .within(div => { + cy.contains(name); + }); +}); + +Then('user sees {string} node side panel', (name: string) => { + cy.get('#loading_kiali_spinner').should('not.exist'); + cy.get('#target-panel-node') + .should('be.visible') + .within(div => { + cy.contains(name); + }); +}); + // Since I can't import from MeshElems.tsx, copying some helpers here... const elems = (c: Controller): { edges: Edge[]; nodes: Node[] } => { const elems = c.getElements(); diff --git a/frontend/cypress/integration/featureFiles/mesh.feature b/frontend/cypress/integration/featureFiles/mesh.feature index 36f9de2c68..05d4c964c0 100644 --- a/frontend/cypress/integration/featureFiles/mesh.feature +++ b/frontend/cypress/integration/featureFiles/mesh.feature @@ -27,9 +27,33 @@ Feature: Kiali Mesh page And user sees expected mesh infra @selected + Scenario: Test istiod + When user selects mesh node with label "istiod-default" + Then user sees control plane side panel + + Scenario: Grafana Infra + When user selects mesh node with label "Grafana" + Then user sees "Grafana" node side panel + + Scenario: Jaeger Infra + When user selects mesh node with label "jaeger" + Then user sees "jaeger" node side panel + + Scenario: Prometheus Infra + When user selects mesh node with label "Prometheus" + Then user sees "Prometheus" node side panel + Scenario: Test DataPlane When user selects mesh node with label "Data Plane" - Then user sees data plane side panel + Then user sees "Kubernetes" cluster side panel + + Scenario: Test Kubernetes + When user selects mesh node with label "Kubernetes" + Then user sees "Kubernetes" cluster side panel + + Scenario: Test istio-system + When user selects mesh node with label "istio-system" + Then user sees "istio-system" namespace side panel # @bookinfo-app # Scenario: See DataPlane \ No newline at end of file diff --git a/frontend/src/pages/Mesh/Mesh.tsx b/frontend/src/pages/Mesh/Mesh.tsx index 9af38ac2ae..853738606f 100644 --- a/frontend/src/pages/Mesh/Mesh.tsx +++ b/frontend/src/pages/Mesh/Mesh.tsx @@ -35,7 +35,7 @@ import { HistoryManager, URLParam } from 'app/History'; import { TourStop } from 'components/Tour/TourStop'; import { getFocusSelector, unsetFocusSelector } from 'utils/SearchParamUtils'; import { meshComponentFactory } from './components/meshComponentFactory'; -import { MeshData } from './MeshPage'; +import { MeshData, MeshRefs } from './MeshPage'; import { MeshInfraType, MeshTarget } from 'types/Mesh'; import { MeshHighlighter } from './MeshHighlighter'; import { @@ -96,7 +96,7 @@ const TopologyContent: React.FC<{ meshData: MeshData; onEdgeTap?: (edge: Edge) => void; onNodeTap?: (node: Node) => void; - onReady: (controller: any, setSelectedIds: (value: string[]) => void) => void; + onReady: (refs: MeshRefs) => void; setLayout: (val: LayoutName) => void; setTarget: (meshTarget: MeshTarget) => void; setUpdateTime: (val: TimeInMilliseconds) => void; @@ -411,7 +411,7 @@ const TopologyContent: React.FC<{ if (initialGraph) { console.debug('mesh onReady'); - onReady(controller, setSelectedIds); + onReady({ controller: controller, setSelectedIds: setSelectedIds }); } // notify that the graph has been updated @@ -568,7 +568,7 @@ export const Mesh: React.FC<{ meshData: MeshData; onEdgeTap?: (edge: Edge) => void; onNodeTap?: (node: Node) => void; - onReady: (controller: any, setSelectedIds: (values: string[]) => void) => void; + onReady: (refs: MeshRefs) => void; setLayout: (layout: Layout) => void; setTarget: (meshTarget: MeshTarget) => void; setUpdateTime: (val: TimeInMilliseconds) => void; diff --git a/frontend/src/pages/Mesh/MeshPage.tsx b/frontend/src/pages/Mesh/MeshPage.tsx index 0d3b696133..fb832b7a8a 100644 --- a/frontend/src/pages/Mesh/MeshPage.tsx +++ b/frontend/src/pages/Mesh/MeshPage.tsx @@ -81,10 +81,16 @@ export type MeshData = { timestamp: TimeInMilliseconds; }; +// MeshRefs 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 MeshRefs = { + controller: Controller; + setSelectedIds: (values: string[]) => void; +}; + type MeshPageState = { - controller?: Controller; meshData: MeshData; - setSelectedIds?: (values: string[]) => void; + meshRefs?: MeshRefs; }; const containerStyle = kialiStyle({ @@ -141,7 +147,6 @@ class MeshPageComponent extends React.Component { this.meshDataSource = new MeshDataSource(); this.state = { - controller: undefined, meshData: { elements: { edges: [], nodes: [] }, elementsChanged: false, @@ -215,7 +220,7 @@ class MeshPageComponent extends React.Component {
{ console.debug(`onFocus(${focusNode})`); }; - private handleReady = (controller: Controller, setSelectedIds: (values: string[]) => void) => { - this.setState({ controller: controller, setSelectedIds: setSelectedIds }); + private handleReady = (refs: MeshRefs) => { + this.setState({ meshRefs: refs }); }; private handleEmptyMeshAction = () => { diff --git a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx index ce04ae722e..f208da9627 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx @@ -137,7 +137,7 @@ class TargetPanelControlPlaneComponent extends React.Component< const data = this.state.controlPlaneNode?.getData() as NodeData; return ( -
+
+
{this.renderNodeHeader(data)}
diff --git a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx index dab306423f..56e9ae6b16 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx @@ -157,7 +157,7 @@ export class TargetPanelNamespace extends React.Component +
+
{renderNodeHeader(data, this.props.t, isExternal(data.cluster))}
{data.version && ( From 10ed534c4576b134921ac0d242668f345729b239 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Wed, 8 May 2024 12:05:23 -0400 Subject: [PATCH 22/46] - fix PF Badge on DataPlane node. - add icon on DataPlane node. - small refactor from if-then to switch stmt --- frontend/src/components/Pf/PfBadges.tsx | 1 + frontend/src/pages/Mesh/MeshElems.tsx | 2 +- frontend/src/pages/Mesh/styles/MeshNode.tsx | 45 +++++++++++++-------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/Pf/PfBadges.tsx b/frontend/src/components/Pf/PfBadges.tsx index c5045de336..5ec017b193 100644 --- a/frontend/src/components/Pf/PfBadges.tsx +++ b/frontend/src/components/Pf/PfBadges.tsx @@ -19,6 +19,7 @@ export const PFBadges: { [key: string]: PFBadgeType } = Object.freeze({ Cluster: { badge: 'C', tt: 'Cluster', style: { backgroundColor: PFColors.Blue300 } } as PFBadgeType, ClusterRBACConfig: { badge: 'CRC', tt: 'Cluster RBAC Configuration' } as PFBadgeType, Container: { badge: 'C', tt: 'Container', style: { backgroundColor: PFColors.Blue300 } } as PFBadgeType, + DataPlane: { badge: 'DP', tt: 'Data Plane' } as PFBadgeType, DestinationRule: { badge: 'DR', tt: 'Destination Rule' } as PFBadgeType, EnvoyFilter: { badge: 'EF', tt: 'Envoy Filter' } as PFBadgeType, ExternalService: { badge: 'ES', tt: 'External Service' } as PFBadgeType, diff --git a/frontend/src/pages/Mesh/MeshElems.tsx b/frontend/src/pages/Mesh/MeshElems.tsx index ef2901edb0..e194602c56 100644 --- a/frontend/src/pages/Mesh/MeshElems.tsx +++ b/frontend/src/pages/Mesh/MeshElems.tsx @@ -138,7 +138,7 @@ export const setNodeLabel = (node: NodeModel, _nodeMap: NodeMap): void => { pfBadge = PFBadges.Cluster; break; case MeshInfraType.DATAPLANE: - pfBadge = PFBadges.DATAPLANE; + pfBadge = PFBadges.DataPlane; content.push(`${(data.infraData as NamespaceInfo[]).length} Namespaces`); break; case MeshInfraType.GRAFANA: diff --git a/frontend/src/pages/Mesh/styles/MeshNode.tsx b/frontend/src/pages/Mesh/styles/MeshNode.tsx index 3d098ae1f9..f7b033eb0b 100644 --- a/frontend/src/pages/Mesh/styles/MeshNode.tsx +++ b/frontend/src/pages/Mesh/styles/MeshNode.tsx @@ -20,6 +20,7 @@ import { ReactComponent as KialiLogo } from '../../../assets/img/mesh/kiali.svg' import { ReactComponent as TempoLogo } from '../../../assets/img/mesh/tempo.svg'; import { store } from 'store/ConfigStore'; import { JAEGER, TEMPO } from 'types/Tracing'; +import { LayerGroupIcon } from '@patternfly/react-icons'; // This is the registered Node component override that utilizes our customized Node.tsx component. @@ -28,26 +29,38 @@ type MeshNodeProps = { } & WithSelectionProps; const renderIcon = (element: Node): React.ReactNode => { - let Component: React.FunctionComponent> | undefined; + let Component: + | React.FunctionComponent> + | React.ComponentClass> + | undefined; const data = element.getData() as MeshNodeData; const externalServices = store.getState().statusState.externalServices; - if (data.infraType === MeshInfraType.GRAFANA) { - Component = GrafanaLogo; - } else if (data.infraType === MeshInfraType.ISTIOD) { - Component = IstioLogo; - } else if (data.infraType === MeshInfraType.TRACE_STORE) { - if (externalServices.find(service => service.name.toLowerCase() === TEMPO)) { - Component = TempoLogo; - } else if (externalServices.find(service => service.name.toLowerCase() === JAEGER)) { - Component = JaegerLogo; - } - } else if (data.infraType === MeshInfraType.KIALI) { - Component = KialiLogo; - } else if (data.infraType === MeshInfraType.METRIC_STORE) { - // TODO: don't assume Prometheus - Component = PrometheusLogo; + switch (data.infraType) { + case MeshInfraType.DATAPLANE: + Component = LayerGroupIcon; + break; + case MeshInfraType.GRAFANA: + Component = GrafanaLogo; + break; + case MeshInfraType.ISTIOD: + Component = IstioLogo; + break; + case MeshInfraType.TRACE_STORE: + if (externalServices.find(service => service.name.toLowerCase() === TEMPO)) { + Component = TempoLogo; + } else if (externalServices.find(service => service.name.toLowerCase() === JAEGER)) { + Component = JaegerLogo; + } + break; + case MeshInfraType.KIALI: + Component = KialiLogo; + break; + case MeshInfraType.METRIC_STORE: + // TODO: don't assume Prometheus + Component = PrometheusLogo; + break; } const { width, height } = element.getDimensions(); From d3f931169fb6670a3f6838dca9d1813bba865f5a Mon Sep 17 00:00:00 2001 From: Fernando Hoyos Date: Wed, 8 May 2024 18:27:31 +0200 Subject: [PATCH 23/46] Polish target panel UI styling --- frontend/src/components/Nav/Navigation.tsx | 45 +++-- .../src/pages/Graph/SummaryPanelStyle.tsx | 15 +- .../src/pages/Mesh/target/TargetPanel.tsx | 13 +- .../pages/Mesh/target/TargetPanelCluster.tsx | 29 ++-- .../pages/Mesh/target/TargetPanelCommon.tsx | 44 +---- .../Mesh/target/TargetPanelControlPlane.tsx | 155 ++++++++---------- .../Mesh/target/TargetPanelDataPlane.tsx | 19 ++- .../target/TargetPanelDataPlaneNamespace.tsx | 132 +++++++-------- .../src/pages/Mesh/target/TargetPanelMesh.tsx | 59 ++++--- .../Mesh/target/TargetPanelNamespace.tsx | 47 +++--- .../src/pages/Mesh/target/TargetPanelNode.tsx | 22 ++- 11 files changed, 261 insertions(+), 319 deletions(-) diff --git a/frontend/src/components/Nav/Navigation.tsx b/frontend/src/components/Nav/Navigation.tsx index aef2bb92d6..a6fe312ed0 100644 --- a/frontend/src/components/Nav/Navigation.tsx +++ b/frontend/src/components/Nav/Navigation.tsx @@ -27,13 +27,18 @@ import { Menu } from './Menu'; import { Link } from 'react-router-dom'; import { ExternalServiceInfo } from '../../types/StatusState'; -type PropsType = RouteComponentProps & { +type ReduxStateProps = { + externalServices: ExternalServiceInfo[]; navCollapsed: boolean; - setNavCollapsed: (collapse: boolean) => void; tracingUrl?: string; - externalServices: ExternalServiceInfo[]; }; +type ReduxDispatchProps = { + setNavCollapsed: (collapse: boolean) => void; +}; + +type PropsType = RouteComponentProps & ReduxStateProps & ReduxDispatchProps; + type NavigationState = { isMobileView: boolean; isNavOpenDesktop: boolean; @@ -47,7 +52,7 @@ const flexBoxColumnStyle = kialiStyle({ export class NavigationComponent extends React.Component { static contextTypes = { - router: () => null + router: (): null => null }; constructor(props: PropsType) { @@ -59,43 +64,47 @@ export class NavigationComponent extends React.Component { + setControlledState = (event: Event): void => { if ('navCollapsed' in event) { this.props.setNavCollapsed(this.props.navCollapsed); } }; - goTotracing() { + goTotracing = (): void => { window.open(this.props.tracingUrl, '_blank'); - } + }; - componentDidMount() { + componentDidMount = (): void => { let pageTitle = serverConfig.installationTag ? serverConfig.installationTag : 'Kiali'; if (homeCluster?.name) { pageTitle += ` [${homeCluster?.name}]`; } document.title = pageTitle; - } + }; - isGraph = () => { - return this.props.location.pathname.startsWith('/graph') || this.props.location.pathname.startsWith('/graphpf'); + isGraph = (): boolean => { + return ( + this.props.location.pathname.startsWith('/graph') || + this.props.location.pathname.startsWith('/graphpf') || + this.props.location.pathname.startsWith('/mesh') + ); }; - onNavToggleDesktop = () => { + onNavToggleDesktop = (): void => { this.setState({ isNavOpenDesktop: !this.state.isNavOpenDesktop }); this.props.setNavCollapsed(!this.props.navCollapsed); }; - onNavToggleMobile = () => { + onNavToggleMobile = (): void => { this.setState({ isNavOpenMobile: !this.state.isNavOpenMobile }); }; - onPageResize = ({ mobileView, windowSize }) => { + onPageResize = ({ mobileView, windowSize }: { mobileView: boolean; windowSize: number }): void => { let ismobile = mobileView; if (windowSize < 1000) { ismobile = true; @@ -105,7 +114,7 @@ export class NavigationComponent extends React.Component { const { isNavOpenDesktop, isNavOpenMobile, isMobileView } = this.state; const isNavOpen = isMobileView ? isNavOpenMobile : isNavOpenDesktop || !this.props.navCollapsed; @@ -154,16 +163,16 @@ export class NavigationComponent extends React.Component ); - } + }; } -const mapStateToProps = (state: KialiAppState) => ({ +const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({ navCollapsed: state.userSettings.interface.navCollapse, tracingUrl: state.tracingState.info && state.tracingState.info.url ? state.tracingState.info.url : undefined, externalServices: state.statusState.externalServices }); -const mapDispatchToProps = (dispatch: KialiDispatch) => ({ +const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => ({ setNavCollapsed: (collapse: boolean) => dispatch(UserSettingsThunkActions.setNavCollapsed(collapse)) }); diff --git a/frontend/src/pages/Graph/SummaryPanelStyle.tsx b/frontend/src/pages/Graph/SummaryPanelStyle.tsx index 2bceaf37a2..4fc64eaa75 100644 --- a/frontend/src/pages/Graph/SummaryPanelStyle.tsx +++ b/frontend/src/pages/Graph/SummaryPanelStyle.tsx @@ -2,7 +2,7 @@ import { PFColors } from 'components/Pf/PfColors'; import { kialiStyle } from 'styles/StyleUtils'; export const panelStyle = kialiStyle({ - marginBottom: '23px', + marginBottom: '1.5rem', border: `1px solid ${PFColors.BorderColor100}`, borderRadius: '1px', '-webkit-box-shadow': '0 1px 1px rgba(0, 0, 0, 0.05)', @@ -10,15 +10,14 @@ export const panelStyle = kialiStyle({ }); export const panelHeadingStyle = kialiStyle({ - padding: '10px 15px', - borderBottom: '1px solid transparent', + padding: '0.5rem 1rem', + borderBottom: `1px solid ${PFColors.BorderColor100}`, borderTopLeftRadius: 0, - borderTopRightRadius: 0, - borderColor: PFColors.BorderColor100 + borderTopRightRadius: 0 }); export const panelBodyStyle = kialiStyle({ - padding: '15px', + padding: '1rem', $nest: { '&:after, &:before': { display: 'table', @@ -27,6 +26,10 @@ export const panelBodyStyle = kialiStyle({ '&:after': { clear: 'both' + }, + + '& pre': { + whiteSpace: 'pre-wrap' } } }); diff --git a/frontend/src/pages/Mesh/target/TargetPanel.tsx b/frontend/src/pages/Mesh/target/TargetPanel.tsx index 4054bc0993..1204e996ab 100644 --- a/frontend/src/pages/Mesh/target/TargetPanel.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanel.tsx @@ -8,7 +8,7 @@ import { FocusNode } from 'pages/GraphPF/GraphPF'; import { classes } from 'typestyle'; import { PFColors } from 'components/Pf/PfColors'; import { MeshInfraType, MeshTarget, MeshType } from 'types/Mesh'; -import { TargetPanelCommonProps, targetPanel } from './TargetPanelCommon'; +import { TargetPanelCommonProps, targetPanelStyle } from './TargetPanelCommon'; import { MeshTourStops } from '../MeshHelpTour'; import { BoxByType } from 'types/Graph'; import { ElementModel, GraphElement } from '@patternfly/react-topology'; @@ -26,6 +26,7 @@ type TargetPanelState = { }; type ReduxProps = { + kiosk: string; meshStatus: string; minTLS: string; }; @@ -47,7 +48,7 @@ const expandedStyle = kialiStyle({ height: '100%' }); const collapsedStyle = kialiStyle({ $nest: { - ['& > .' + targetPanel]: { + [`& > .${targetPanelStyle}`]: { display: 'none' } } @@ -81,7 +82,7 @@ class TargetPanelComponent extends React.Component { + private getTargetPanel = (target: MeshTarget): React.ReactNode => { const targetType = target.type as MeshType; switch (targetType) { @@ -203,14 +204,14 @@ class TargetPanelComponent extends React.Component { + private togglePanel = (): void => { this.setState((state: TargetPanelState) => ({ isCollapsed: !state.isCollapsed })); }; } -const mapStateToProps = (state: KialiAppState) => ({ +const mapStateToProps = (state: KialiAppState): ReduxProps => ({ kiosk: state.globalState.kiosk, meshStatus: meshWideMTLSStatusSelector(state), minTLS: minTLSVersionSelector(state) diff --git a/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx b/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx index f915e92412..eae9085f9c 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx @@ -4,15 +4,7 @@ import { kialiStyle } from 'styles/StyleUtils'; import { PFColors } from 'components/Pf/PfColors'; import { PFBadge, PFBadges } from 'components/Pf/PfBadges'; import { getKialiTheme } from 'utils/ThemeUtils'; -import { - TargetPanelCommonProps, - shouldRefreshData, - targetPanel, - targetPanelBody, - targetPanelBorder, - targetPanelHeading, - targetPanelWidth -} from './TargetPanelCommon'; +import { TargetPanelCommonProps, shouldRefreshData, targetPanelStyle, targetPanelWidth } from './TargetPanelCommon'; import { kialiIconDark, kialiIconLight } from 'config'; import { KialiInstance, MeshNodeData, isExternal } from 'types/Mesh'; import { I18N_NAMESPACE, Theme } from 'types/Common'; @@ -26,6 +18,7 @@ import { classes } from 'typestyle'; import { descendents } from '../MeshElems'; import { renderNodeHeader } from './TargetPanelNode'; import { WithTranslation, withTranslation } from 'react-i18next'; +import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; type TargetPanelClusterProps = WithTranslation & TargetPanelCommonProps; @@ -73,21 +66,21 @@ class TargetPanelClusterComponent extends React.Component -
+
+
{clusterData.isKialiHome && ( @@ -112,7 +105,7 @@ class TargetPanelClusterComponent extends React.Component {isExternal(data.cluster) ? ( -
+
{descendents(this.state.clusterNode) .sort((n1, n2) => { const name1 = (n1.getData() as MeshNodeData).infraName.toLowerCase(); @@ -124,7 +117,7 @@ class TargetPanelClusterComponent extends React.Component ) : ( -
+
{clusterData.accessible && this.renderKialiLinks(clusterData.kialiInstances)} {version && ( <> @@ -174,7 +167,7 @@ class TargetPanelClusterComponent extends React.Component { const kialiIcon = getKialiTheme() === Theme.DARK ? kialiIconDark : kialiIconLight; - return kialiInstances.map(instance => { + return kialiInstances?.map(instance => { if (instance.url.length !== 0) { return ( diff --git a/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx b/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx index c346a0639c..178ec31d7d 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelCommon.tsx @@ -20,55 +20,21 @@ export interface TargetPanelCommonProps { export const targetPanelWidth = '35rem'; -export const targetPanel = kialiStyle({ +export const targetPanelStyle = kialiStyle({ fontSize: 'var(--graph-side-panel--font-size)', height: '100%', margin: 0, minWidth: targetPanelWidth, - overflowY: 'scroll', + overflowY: 'auto', padding: 0, position: 'relative', width: targetPanelWidth }); -export const targetPanelBody = kialiStyle({ - padding: '15px', - $nest: { - '&:after, &:before': { - display: 'table', - content: ' ' - }, - - '&:after': { - clear: 'both' - } - } -}); - -export const targetPanelBorder = kialiStyle({ - marginBottom: '23px', - border: `1px solid ${PFColors.BorderColor100}`, - borderRadius: '1px', - '-webkit-box-shadow': '0 1px 1px rgba(0, 0, 0, 0.05)', - boxShadow: '0 1px 1px rgba(0, 0, 0, 0.05)' -}); - export const targetPanelFont: React.CSSProperties = { fontSize: 'var(--graph-side-panel--font-size)' }; -export const targetPanelHeading = kialiStyle({ - padding: '10px 15px', - borderBottom: '1px solid transparent', - borderTopLeftRadius: 0, - borderTopRightRadius: 0, - borderColor: PFColors.BorderColor100 -}); - -export const TargetPanelTabs = kialiStyle({ - padding: '0.5rem 1rem 0 1rem' -}); - export const targetPanelTitle = kialiStyle({ fontWeight: 'bolder', marginTop: '0.25rem', @@ -83,12 +49,10 @@ const healthStatusStyle = kialiStyle({ const hrStyle = kialiStyle({ border: 0, borderTop: `1px solid ${PFColors.BorderColor100}`, - margin: '1.0rem 0' + margin: '1rem 0' }); -export const targetPanelHR = (): React.ReactNode => { - return
; -}; +export const targetPanelHR =
; export const shouldRefreshData = (prevProps: TargetPanelCommonProps, nextProps: TargetPanelCommonProps): boolean => { return ( diff --git a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx index f208da9627..3aaeab1ec1 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx @@ -5,13 +5,11 @@ import { TargetPanelCommonProps, getHealthStatus, shouldRefreshData, - targetPanel, - targetPanelBody, - targetPanelBorder, - targetPanelHR + targetPanelHR, + targetPanelStyle } from './TargetPanelCommon'; import { PFBadge, PFBadges } from 'components/Pf/PfBadges'; -import { Card, CardBody, CardHeader, Title, TitleSizes } from '@patternfly/react-core'; +import { Title, TitleSizes } from '@patternfly/react-core'; import { serverConfig } from 'config'; import { CanaryUpgradeStatus, OutboundTrafficPolicy } from 'types/IstioObjects'; import { NamespaceInfo, NamespaceStatus } from 'types/NamespaceInfo'; @@ -35,7 +33,7 @@ import * as FilterHelper from '../../../components/FilterList/FilterHelper'; import { NodeData } from '../MeshElems'; import { ControlPlaneMetricsMap, Metric } from 'types/Metrics'; import { classes } from 'typestyle'; -import { panelHeadingStyle } from 'pages/Graph/SummaryPanelStyle'; +import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { MeshMTLSStatus } from 'components/MTls/MeshMTLSStatus'; import { WithTranslation, withTranslation } from 'react-i18next'; import { I18N_NAMESPACE } from 'types/Common'; @@ -76,12 +74,6 @@ const defaultState: TargetPanelControlPlaneState = { // TODO: Should these remain fixed values? const direction: DirectionType = 'outbound'; -const cardGridStyle = kialiStyle({ - marginBottom: '0.5rem', - marginTop: 0, - textAlign: 'center' -}); - const nodeStyle = kialiStyle({ alignItems: 'center', display: 'flex' @@ -137,73 +129,66 @@ class TargetPanelControlPlaneComponent extends React.Component< const data = this.state.controlPlaneNode?.getData() as NodeData; return ( -
- - - - <span className={nodeStyle}> - <PFBadge badge={PFBadges.Istio} size="global" /> - {data.infraName} - {getHealthStatus(data, this.props.t)} - </span> - +
+
+ <span className={nodeStyle}> - <PFBadge badge={PFBadges.Namespace} size="sm" /> - {data.namespace} + <PFBadge badge={PFBadges.Istio} size="global" /> + {data.infraName} + {getHealthStatus(data, this.props.t)} </span> - <span className={nodeStyle}> - <PFBadge badge={PFBadges.Cluster} size="sm" /> - {data.cluster} - </span> - </CardHeader> - <CardBody> - <div className={targetPanelBody}> - {data.version && ( - <div style={{ textAlign: 'left' }}> - {`Version: `} - {data.version} - <br /> - </div> - )} - <div style={{ textAlign: 'left' }}> - <div> - <MeshMTLSStatus /> - </div> - </div> - - <ControlPlaneNamespaceStatus - outboundTrafficPolicy={this.state.outboundPolicyMode} - namespace={nsInfo} - ></ControlPlaneNamespaceStatus> - - <TLSInfo - certificatesInformationIndicators={ - serverConfig.kialiFeatureFlags.certificatesInformationIndicators.enabled - } - version={this.props.minTLS} - ></TLSInfo> - - {!isRemoteCluster(nsInfo.annotations) && ( + + + + {data.namespace} + + + + {data.cluster} + +
+
+ {data.version && ( +
+ {`Version: `} + {data.version} +
+
+ )} +
+
+ +
+
+ + + + + + {!isRemoteCluster(nsInfo.annotations) && ( +
+ {targetPanelHR} + {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && (
- {targetPanelHR()} - {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && ( -
- {targetPanelHR} - -
- )} -
{this.props.istioAPIEnabled &&
{this.renderCharts()}
}
+ {targetPanelHR} +
)} +
{this.props.istioAPIEnabled &&
{this.renderCharts()}
}
- - -
+ )} + + {targetPanelHR}
{JSON.stringify(data.infraData, null, 2)}
@@ -212,20 +197,14 @@ class TargetPanelControlPlaneComponent extends React.Component< private getLoading = (): React.ReactNode => { return ( -
- - - - <span className={nodeStyle}> - <span>Loading...</span> - </span> - - - +
+
+ + <span className={nodeStyle}> + <span>Loading...</span> + </span> + +
); }; @@ -444,7 +423,7 @@ class TargetPanelControlPlaneComponent extends React.Component< FilterHelper.handleError(`${message}: ${API.getErrorString(error)}`); } - private renderCharts(): JSX.Element { + private renderCharts(): React.ReactNode { if (this.state.status) { const data = this.state.controlPlaneNode!.getData() as NodeData; return ( diff --git a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx index 7d3eb1ed44..30682d4bbd 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { Node, NodeModel } from '@patternfly/react-topology'; -import { TargetPanelCommonProps, targetPanel, targetPanelBody, targetPanelHeading } from './TargetPanelCommon'; +import { TargetPanelCommonProps, targetPanelHR, targetPanelStyle } from './TargetPanelCommon'; import { classes } from 'typestyle'; import { MeshNodeData } from 'types/Mesh'; -import { panelStyle } from 'pages/Graph/SummaryPanelStyle'; +import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { PFBadge, PFBadges } from 'components/Pf/PfBadges'; import { kialiStyle } from 'styles/StyleUtils'; import { Title, TitleSizes } from '@patternfly/react-core'; @@ -14,14 +14,14 @@ import { serverConfig } from 'config'; type TargetPanelDataPlaneState = { expanded: string[]; - node?: Node; loading: boolean; + node?: Node; }; const defaultState: TargetPanelDataPlaneState = { expanded: [], - node: undefined, - loading: false + loading: false, + node: undefined }; const nodeStyle = kialiStyle({ @@ -37,7 +37,7 @@ export class TargetPanelDataPlane extends React.Component -
{this.renderNodeHeader(data)}
-
+
+
{this.renderNodeHeader(data)}
+
@@ -86,6 +86,7 @@ export class TargetPanelDataPlane extends React.Component + {targetPanelHR}
                             {JSON.stringify(
                               data.infraData.find(id => id.name === ns.name),
diff --git a/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx
index 0f56fb020c..9e3fee1270 100644
--- a/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx
+++ b/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx
@@ -1,12 +1,6 @@
 import * as React from 'react';
 import { kialiStyle } from 'styles/StyleUtils';
-import {
-  TargetPanelCommonProps,
-  targetPanel,
-  targetPanelBody,
-  targetPanelBorder,
-  targetPanelHR
-} from './TargetPanelCommon';
+import { TargetPanelCommonProps, targetPanelHR } from './TargetPanelCommon';
 import { PFBadge, PFBadges } from 'components/Pf/PfBadges';
 import { Card, CardBody, CardHeader, Title, TitleSizes, Tooltip, TooltipPosition } from '@patternfly/react-core';
 import { Paths, serverConfig } from 'config';
@@ -35,8 +29,7 @@ import { switchType } from 'pages/Overview/OverviewHelper';
 import { IstiodResourceThresholds } from 'types/IstioStatus';
 import { TLSStatus } from 'types/TLSStatus';
 import * as FilterHelper from '../../../components/FilterList/FilterHelper';
-import { classes } from 'typestyle';
-import { panelHeadingStyle } from 'pages/Graph/SummaryPanelStyle';
+import { panelBodyStyle, panelHeadingStyle } from 'pages/Graph/SummaryPanelStyle';
 import { Metric } from 'types/Metrics';
 
 type TargetPanelDataPlaneNamespaceProps = Omit & {
@@ -77,7 +70,8 @@ const healthType: OverviewType = 'app';
 const cardGridStyle = kialiStyle({
   textAlign: 'center',
   marginTop: 0,
-  marginBottom: '0.5rem'
+  marginBottom: '-0.5rem',
+  boxShadow: 'none'
 });
 
 const namespaceNameStyle = kialiStyle({
@@ -89,12 +83,6 @@ const namespaceNameStyle = kialiStyle({
   textOverflow: 'ellipsis'
 });
 
-const panel = kialiStyle({
-  fontSize: 'var(--graph-side-panel--font-size)',
-  margin: 0,
-  padding: 0
-});
-
 export class TargetPanelDataPlaneNamespace extends React.Component<
   TargetPanelDataPlaneNamespaceProps,
   TargetPanelDataPlaneNamespaceState
@@ -132,66 +120,60 @@ export class TargetPanelDataPlaneNamespace extends React.Component<
     );
 
     return (
-      
- - {namespaceActions}, hasNoOffset: false, className: undefined }} - > - - <span className={namespaceNameStyle}> - <span> - <PFBadge badge={PFBadges.Namespace} /> - {ns} - </span> - {this.renderNamespaceBadges(nsInfo, true)} + <Card isCompact={true} className={cardGridStyle} data-test={`${ns}-mesh-target`}> + <CardHeader + className={panelHeadingStyle} + actions={{ actions: <>{namespaceActions}</>, hasNoOffset: false, className: undefined }} + > + <Title headingLevel="h5" size={TitleSizes.lg}> + <span className={namespaceNameStyle}> + <span> + <PFBadge badge={PFBadges.Namespace} /> + {ns} </span> - -
- - {nsInfo.cluster} -
-
- -
- {this.renderLabels(nsInfo)} - -
-
Istio config
- - {nsInfo.tlsStatus && ( - - - - )} - {this.props.istioAPIEnabled ? this.renderIstioConfigStatus(nsInfo) : 'N/A'} -
- - {this.renderStatus()} - - {targetPanelHR()} - {this.renderCharts('inbound')} - {this.renderCharts('outbound')} -
-
-
-
+ {this.renderNamespaceBadges(nsInfo, true)} + + +
+ + {nsInfo.cluster} +
+ + + {this.renderLabels(nsInfo)} + +
+
Istio config
+ + {nsInfo.tlsStatus && ( + + + + )} + {this.props.istioAPIEnabled ? this.renderIstioConfigStatus(nsInfo) : 'N/A'} +
+ + {this.renderStatus()} + + {targetPanelHR} + {this.renderCharts('inbound')} + {this.renderCharts('outbound')} +
+ ); } private getLoading = (): React.ReactNode => { return ( -
- - - - <span className={namespaceNameStyle}> - <span>Loading...</span> - </span> - - - -
+ + + + <span className={namespaceNameStyle}> + <span>Loading...</span> + </span> + + + ); }; @@ -269,7 +251,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< return namespaceActions; }; - private renderNamespaceBadges(ns: NamespaceInfo, tooltip: boolean): JSX.Element { + private renderNamespaceBadges(ns: NamespaceInfo, tooltip: boolean): React.ReactNode { return ( <> {serverConfig.ambientEnabled && ns.labels && ns.isAmbient && ( @@ -279,7 +261,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< ); } - private renderLabels(ns: NamespaceInfo): JSX.Element { + private renderLabels(ns: NamespaceInfo): React.ReactNode { const labelsLength = ns.labels ? `${Object.entries(ns.labels).length}` : 'No'; const labelContent = ns.labels ? ( @@ -311,7 +293,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< return labelContent; } - private renderIstioConfigStatus(ns: NamespaceInfo): JSX.Element { + private renderIstioConfigStatus(ns: NamespaceInfo): React.ReactNode { let validations: ValidationStatus = { errors: 0, namespace: ns.name, objectCount: 0, warnings: 0 }; if (!!ns.validations) { @@ -449,7 +431,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< FilterHelper.handleError(`${message}: ${API.getErrorString(error)}`); } - private renderCharts(direction: DirectionType): JSX.Element { + private renderCharts(direction: DirectionType): React.ReactNode { if (this.state.status) { const namespace = this.props.targetNamespace; return ( @@ -469,7 +451,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< return
; } - private renderStatus(): JSX.Element { + private renderStatus(): React.ReactNode { const targetPage = switchType(healthType, Paths.APPLICATIONS, Paths.SERVICES, Paths.WORKLOADS); const namespace = this.props.targetNamespace; const status = this.state.status; diff --git a/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx b/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx index dd048b5b32..322fdd61d0 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import { Visualization } from '@patternfly/react-topology'; -import { - TargetPanelCommonProps, - getTitle, - targetPanel, - targetPanelBorder, - targetPanelHeading -} from './TargetPanelCommon'; +import { GraphElement, Visualization } from '@patternfly/react-topology'; +import { TargetPanelCommonProps, getTitle, targetPanelStyle } from './TargetPanelCommon'; import { classes } from 'typestyle'; +import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; +import { elems, selectAnd } from '../MeshElems'; +import { MeshAttr, MeshInfraType } from 'types/Mesh'; +import { renderNodeHeader } from './TargetPanelNode'; +import { WithTranslation, withTranslation } from 'react-i18next'; +import { I18N_NAMESPACE } from 'types/Common'; type TargetPanelMeshState = { loading: boolean; @@ -19,38 +19,55 @@ const defaultState: TargetPanelMeshState = { loading: false }; -export class TargetPanelMesh extends React.Component { - constructor(props: TargetPanelCommonProps) { +type TargetPanelMeshProps = WithTranslation & TargetPanelCommonProps; + +class TargetPanelMeshComponent extends React.Component { + constructor(props: TargetPanelMeshProps) { super(props); this.state = { ...defaultState }; } - static getDerivedStateFromProps(props: TargetPanelCommonProps, state: TargetPanelMeshState) { + static getDerivedStateFromProps: React.GetDerivedStateFromProps = ( + props: TargetPanelCommonProps, + state: TargetPanelMeshState + ) => { // if the target (i.e. mesh) 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.mesh ? { graph: props.target.elem, loading: true } : null; - } - - componentDidMount() {} - - componentDidUpdate(_prevProps: TargetPanelCommonProps) {} - - componentWillUnmount() {} + }; - render() { + render(): React.ReactNode { const controller = this.props.target.elem as Visualization; if (!controller) { return null; } + const { nodes } = elems(controller); + + const infraNodes = selectAnd(nodes, [ + { prop: MeshAttr.infraType, op: '!=', val: MeshInfraType.CLUSTER }, + { prop: MeshAttr.infraType, op: '!=', val: MeshInfraType.NAMESPACE }, + { prop: MeshAttr.infraType, op: '!=', val: MeshInfraType.DATAPLANE }, + { prop: MeshAttr.infraType, op: '!=', val: '' } + ]); + return ( -
-
+
+
{getTitle(`Mesh Name: ${controller.getGraph().getData().meshData.name}`)}
+
+ {this.renderMeshSummary(infraNodes)} +
); } + + private renderMeshSummary = (infraNodes: GraphElement[]): React.ReactNode => ( + <>{infraNodes.map(node => renderNodeHeader(node.getData(), this.props.t, true))} + ); } + +export const TargetPanelMesh = withTranslation(I18N_NAMESPACE)(TargetPanelMeshComponent); diff --git a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx index 56e9ae6b16..46d07404d1 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx @@ -1,14 +1,7 @@ import * as React from 'react'; import { ElementModel, GraphElement, Node, NodeModel } from '@patternfly/react-topology'; import { kialiStyle } from 'styles/StyleUtils'; -import { - TargetPanelCommonProps, - shouldRefreshData, - targetPanel, - targetPanelBody, - targetPanelBorder, - targetPanelHR -} from './TargetPanelCommon'; +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 { Paths, serverConfig } from 'config'; @@ -45,7 +38,7 @@ import { TLSStatus } from 'types/TLSStatus'; import * as FilterHelper from '../../../components/FilterList/FilterHelper'; import { ControlPlaneMetricsMap, Metric } from 'types/Metrics'; import { classes } from 'typestyle'; -import { panelHeadingStyle } from 'pages/Graph/SummaryPanelStyle'; +import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; type TargetPanelNamespaceProps = TargetPanelCommonProps; @@ -85,7 +78,8 @@ const direction: DirectionType = 'outbound'; const cardGridStyle = kialiStyle({ textAlign: 'center', marginTop: 0, - marginBottom: '0.5rem' + marginBottom: '0.5rem', + boxShadow: 'none' }); const namespaceNameStyle = kialiStyle({ @@ -157,8 +151,9 @@ export class TargetPanelNamespace extends React.Component +
- + {isControlPlane && !isRemoteCluster(nsInfo.annotations) && ( -
+ <> {this.renderLabels(nsInfo)}
@@ -199,7 +194,7 @@ export class TargetPanelNamespace extends React.Component - {targetPanelHR()} + {targetPanelHR} {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && (
{targetPanelHR} @@ -209,11 +204,11 @@ export class TargetPanelNamespace extends React.Component{this.props.istioAPIEnabled &&
{this.renderCharts()}
}
)} -
+ )} {isControlPlane && isRemoteCluster(nsInfo.annotations) && ( -
+ <> {this.renderLabels(nsInfo)}
@@ -231,11 +226,11 @@ export class TargetPanelNamespace extends React.Component -
+ )} {!isControlPlane && ( -
+ <> {this.renderLabels(nsInfo)}
@@ -251,9 +246,9 @@ export class TargetPanelNamespace extends React.Component + )} @@ -263,7 +258,7 @@ export class TargetPanelNamespace extends React.Component { return ( -
+
@@ -616,7 +611,7 @@ export class TargetPanelNamespace extends React.Component; } - private renderStatus(): JSX.Element { + private renderStatus(): React.ReactNode { const targetPage = switchType(healthType, Paths.APPLICATIONS, Paths.SERVICES, Paths.WORKLOADS); const namespace = this.state.targetNamespace!; const status = this.state.status; diff --git a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx index dac930ff50..db7243d9dc 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx @@ -1,17 +1,11 @@ import * as React from 'react'; import { Node, NodeModel } from '@patternfly/react-topology'; import { kialiStyle } from 'styles/StyleUtils'; -import { - TargetPanelCommonProps, - getHealthStatus, - targetPanel, - targetPanelBody, - targetPanelHeading -} from './TargetPanelCommon'; +import { TargetPanelCommonProps, getHealthStatus, targetPanelStyle } from './TargetPanelCommon'; import { PFBadge, PFBadges } from 'components/Pf/PfBadges'; import { MeshInfraType, MeshNodeData, isExternal } from 'types/Mesh'; import { classes } from 'typestyle'; -import { panelStyle } from 'pages/Graph/SummaryPanelStyle'; +import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { Title, TitleSizes } from '@patternfly/react-core'; import { WithTranslation, withTranslation } from 'react-i18next'; import { I18N_NAMESPACE } from 'types/Common'; @@ -58,8 +52,12 @@ export function renderNodeHeader( case MeshInfraType.TRACE_STORE: pfBadge = PFBadges.TraceStore; break; + case MeshInfraType.ISTIOD: + pfBadge = PFBadges.Istio; + break; default: console.warn(`MeshElems: Unexpected infraType [${data.infraType}] `); + pfBadge = PFBadges.Unknown; } return ( @@ -68,7 +66,7 @@ export function renderNodeHeader( {data.infraName} - {!nameOnly && getHealthStatus(data, t)} + {getHealthStatus(data, t)} {!nameOnly && ( @@ -118,9 +116,9 @@ class TargetPanelNodeComponent extends React.Component -
{renderNodeHeader(data, this.props.t, isExternal(data.cluster))}
-
+
+
{renderNodeHeader(data, this.props.t, isExternal(data.cluster))}
+
{data.version && (
{`Version: `} From fdf2c90eaad2c41cff9b3ea042146402a579afda Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Wed, 8 May 2024 16:19:56 -0400 Subject: [PATCH 24/46] change approach for grabbing meshpagecomponent to be more predictable and to hopefully work both locally and in CI. --- .../cypress/integration/common/mesh_test.ts | 25 +++---------------- .../integration/featureFiles/mesh.feature | 12 ++++++--- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/frontend/cypress/integration/common/mesh_test.ts b/frontend/cypress/integration/common/mesh_test.ts index fccc9e67db..ce3ff51889 100644 --- a/frontend/cypress/integration/common/mesh_test.ts +++ b/frontend/cypress/integration/common/mesh_test.ts @@ -1,8 +1,5 @@ import { Before, Then, When } from '@badeball/cypress-cucumber-preprocessor'; import { Controller, Edge, Node, Visualization, isEdge, isNode } from '@patternfly/react-topology'; -import { MeshNodeData, MeshTarget } from '../../../src/types/Mesh'; - -const url = '/console'; Before(() => { // Copied from overview.ts. This prevents cypress from stopping on errors unrelated to the tests. @@ -20,13 +17,6 @@ Before(() => { }); }); -//When( -// 'user asks for mesh with refresh {string} and duration {string}', -// (namespaces: string, refresh: string, duration: string) => { -// cy.visit(`${url}/mesh?refresh=${refresh}&duration=${duration}&namespaces=${namespaces}`); -// } -//); - When('user opens mesh tour', () => { cy.get('button#mesh-tour').click(); }); @@ -36,15 +26,9 @@ When('user closes mesh tour', () => { }); When('user selects mesh node with label {string}', (label: string) => { - cy.get('#target-panel-mesh') - .should('be.visible') - .within(div => { - cy.contains('Mesh Name: Istio Mesh'); - }); cy.waitForReact(); - cy.getReact('MeshPageComponent') - .should('have.length', '2') - .nthNode(1) + cy.getReact('MeshPageComponent', { state: { meshData: { isLoading: false } } }) + .should('have.length', 1) .getCurrentState() .then(state => { const controller = state.meshRefs.controller as Visualization; @@ -100,9 +84,8 @@ Then('mesh side panel is shown', () => { Then('user sees expected mesh infra', () => { cy.waitForReact(); - cy.getReact('MeshPageComponent') - .should('have.length', '2') - .nthNode(1) + cy.getReact('MeshPageComponent', { state: { meshData: { isLoading: false } } }) + .should('have.length', 1) .getCurrentState() .then(state => { const controller = state.meshRefs.controller; diff --git a/frontend/cypress/integration/featureFiles/mesh.feature b/frontend/cypress/integration/featureFiles/mesh.feature index 05d4c964c0..4d0af9cb1a 100644 --- a/frontend/cypress/integration/featureFiles/mesh.feature +++ b/frontend/cypress/integration/featureFiles/mesh.feature @@ -22,6 +22,7 @@ Feature: Kiali Mesh page And user closes mesh tour Then user "does not see" mesh tour + @selected Scenario: See mesh Then mesh side panel is shown And user sees expected mesh infra @@ -31,29 +32,32 @@ Feature: Kiali Mesh page When user selects mesh node with label "istiod-default" Then user sees control plane side panel + @selected Scenario: Grafana Infra When user selects mesh node with label "Grafana" Then user sees "Grafana" node side panel + @selected Scenario: Jaeger Infra When user selects mesh node with label "jaeger" Then user sees "jaeger" node side panel + @selected Scenario: Prometheus Infra When user selects mesh node with label "Prometheus" Then user sees "Prometheus" node side panel + @selected Scenario: Test DataPlane When user selects mesh node with label "Data Plane" - Then user sees "Kubernetes" cluster side panel + Then user sees data plane side panel + @selected Scenario: Test Kubernetes When user selects mesh node with label "Kubernetes" Then user sees "Kubernetes" cluster side panel + @selected Scenario: Test istio-system When user selects mesh node with label "istio-system" Then user sees "istio-system" namespace side panel - - # @bookinfo-app - # Scenario: See DataPlane \ No newline at end of file From 96e38c557b2802fe0e304b7cecb66d5c2cf7a5e2 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Wed, 8 May 2024 16:56:13 -0400 Subject: [PATCH 25/46] Fix a couple of Find/Hide issues given ongoing changes --- .../integration/featureFiles/mesh.feature | 10 ------- frontend/src/pages/Mesh/MeshHelpFind.tsx | 10 +++---- frontend/src/pages/Mesh/toolbar/MeshFind.tsx | 27 +++++-------------- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/frontend/cypress/integration/featureFiles/mesh.feature b/frontend/cypress/integration/featureFiles/mesh.feature index 4d0af9cb1a..96d237d728 100644 --- a/frontend/cypress/integration/featureFiles/mesh.feature +++ b/frontend/cypress/integration/featureFiles/mesh.feature @@ -11,53 +11,43 @@ Feature: Kiali Mesh page # NOTE: Mesh Find/Hide has its own feature file - @selected Scenario: Open mesh Tour When user opens mesh tour Then user "sees" mesh tour - @selected Scenario: Close mesh Tour When user opens mesh tour And user closes mesh tour Then user "does not see" mesh tour - @selected Scenario: See mesh Then mesh side panel is shown And user sees expected mesh infra - @selected Scenario: Test istiod When user selects mesh node with label "istiod-default" Then user sees control plane side panel - @selected Scenario: Grafana Infra When user selects mesh node with label "Grafana" Then user sees "Grafana" node side panel - @selected Scenario: Jaeger Infra When user selects mesh node with label "jaeger" Then user sees "jaeger" node side panel - @selected Scenario: Prometheus Infra When user selects mesh node with label "Prometheus" Then user sees "Prometheus" node side panel - @selected Scenario: Test DataPlane When user selects mesh node with label "Data Plane" Then user sees data plane side panel - @selected Scenario: Test Kubernetes When user selects mesh node with label "Kubernetes" Then user sees "Kubernetes" cluster side panel - @selected Scenario: Test istio-system When user selects mesh node with label "istio-system" Then user sees "istio-system" namespace side panel diff --git a/frontend/src/pages/Mesh/MeshHelpFind.tsx b/frontend/src/pages/Mesh/MeshHelpFind.tsx index f67915b274..e4093f2b1a 100644 --- a/frontend/src/pages/Mesh/MeshHelpFind.tsx +++ b/frontend/src/pages/Mesh/MeshHelpFind.tsx @@ -100,14 +100,12 @@ export const MeshHelpFind: React.FC = (props: MeshHelpFindPro const nodeColumns: ThProps[] = [{ title: 'Expression' }, { title: 'Notes' }]; const nodeRows: IRow[] = [ - { cells: ['cluster '] }, + { cells: ['cluster ', 'nodes within the matching clusters'] }, { cells: ['label:
) : ( @@ -191,4 +192,4 @@ class TargetPanelClusterComponent extends React.Component { ); }; -export const getHealthStatus = (data: MeshNodeData, t: TFunction): React.ReactNode => { +export const getHealthStatus = (data: MeshNodeData): React.ReactNode => { let healthSeverity: ValidationTypes; switch (data.healthData) { diff --git a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx index 3aaeab1ec1..3c8d0a6280 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx @@ -35,8 +35,8 @@ import { ControlPlaneMetricsMap, Metric } from 'types/Metrics'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { MeshMTLSStatus } from 'components/MTls/MeshMTLSStatus'; -import { WithTranslation, withTranslation } from 'react-i18next'; -import { I18N_NAMESPACE } from 'types/Common'; +import { WithTranslation } from 'react-i18next'; +import { withKialiTranslation } from 'utils/I18nUtils'; type TargetPanelControlPlaneProps = TargetPanelCommonProps & WithTranslation & { @@ -139,18 +139,21 @@ class TargetPanelControlPlaneComponent extends React.Component< {data.infraName} - {getHealthStatus(data, this.props.t)} + {getHealthStatus(data)} + {data.namespace} + {data.cluster}
+
{data.version && (
@@ -159,10 +162,9 @@ class TargetPanelControlPlaneComponent extends React.Component<
)} +
-
- -
+
{!isRemoteCluster(nsInfo.annotations) && ( -
- {targetPanelHR} + <> {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && ( -
+ <> {targetPanelHR} -
+ )} -
{this.props.istioAPIEnabled &&
{this.renderCharts()}
}
-
+ + {this.props.istioAPIEnabled && ( + <> + {targetPanelHR} + {this.renderCharts()} + + )} + )} {targetPanelHR} @@ -231,6 +238,7 @@ class TargetPanelControlPlaneComponent extends React.Component< 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 }); @@ -272,7 +280,7 @@ class TargetPanelControlPlaneComponent extends React.Component< this.setState({ loading: true }); }; - private fetchCanariesStatus(): Promise { + private fetchCanariesStatus = async (): Promise => { if (!this.isControlPlane()) { return Promise.resolve(); } @@ -291,10 +299,11 @@ class TargetPanelControlPlaneComponent extends React.Component< .catch(error => { AlertUtils.addError('Error fetching namespace canary upgrade status.', error, 'default', MessageType.ERROR); }); - } + }; - private fetchHealthStatus(): Promise { + private fetchHealthStatus = async (): Promise => { const data = this.state.controlPlaneNode!.getData() as NodeData; + return API.getClustersAppHealth(data.namespace, this.props.duration, data.cluster) .then(results => { const nsStatus: NamespaceStatus = { @@ -306,6 +315,7 @@ class TargetPanelControlPlaneComponent extends React.Component< }; const rs = results[data.namespace]; + Object.keys(rs).forEach(item => { const health: Health = rs[item]; const status = health.getGlobalStatus(); @@ -325,9 +335,9 @@ class TargetPanelControlPlaneComponent extends React.Component< this.setState({ status: nsStatus }); }) .catch(err => this.handleApiError('Could not fetch namespace health', err)); - } + }; - private fetchIstiodResourceThresholds(): Promise { + private fetchIstiodResourceThresholds = async (): Promise => { if (!this.isControlPlane()) { return Promise.resolve(); } @@ -339,9 +349,9 @@ class TargetPanelControlPlaneComponent extends React.Component< .catch(error => { AlertUtils.addError('Error fetching Istiod resource thresholds.', error, 'default', MessageType.ERROR); }); - } + }; - private fetchMetrics(): Promise { + private fetchMetrics = async (): Promise => { const rateParams = computePrometheusRateParams(this.props.duration, 10); const options: IstioMetricsOptions = { filters: ['request_count', 'request_error_count'], @@ -351,6 +361,7 @@ class TargetPanelControlPlaneComponent extends React.Component< direction: direction, reporter: direction === 'inbound' ? 'destination' : 'source' }; + const data = this.state.controlPlaneNode!.getData() as NodeData; return API.getNamespaceMetrics(data.namespace, options, data.cluster) @@ -366,6 +377,7 @@ class TargetPanelControlPlaneComponent extends React.Component< istiod_process_cpu: rs.data.process_cpu_seconds_total, istiod_process_mem: rs.data.process_resident_memory_bytes }; + this.setState({ controlPlaneMetrics: controlPlaneMetrics, errorMetrics: errorMetrics, @@ -379,14 +391,15 @@ class TargetPanelControlPlaneComponent extends React.Component< } }) .catch(err => this.handleApiError('Could not fetch namespace metrics', err)); - } + }; - private fetchTLS(): Promise { + private fetchTLS = async (): Promise => { if (!this.isControlPlane()) { return Promise.resolve(); } const data = this.state.controlPlaneNode!.getData() as NodeData; + return API.getNamespaceTls(data.namespace, data.cluster) .then(rs => { this.setState({ @@ -398,9 +411,9 @@ class TargetPanelControlPlaneComponent extends React.Component< }); }) .catch(err => this.handleApiError('Could not fetch namespace TLS status', err)); - } + }; - private fetchOutboundTrafficPolicyMode(): Promise { + private fetchOutboundTrafficPolicyMode = async (): Promise => { if (!this.isControlPlane()) { return Promise.resolve(); } @@ -412,20 +425,21 @@ class TargetPanelControlPlaneComponent extends React.Component< .catch(error => { AlertUtils.addError('Error fetching Mesh OutboundTrafficPolicy.Mode.', error, 'default', MessageType.ERROR); }); - } + }; private isControlPlane = (): boolean => { const data = this.state.controlPlaneNode!.getData() as NodeData; return data.namespace === serverConfig.istioNamespace; }; - private handleApiError(message: string, error: ApiError): void { + private handleApiError = (message: string, error: ApiError): void => { FilterHelper.handleError(`${message}: ${API.getErrorString(error)}`); - } + }; - private renderCharts(): React.ReactNode { + private renderCharts = (): React.ReactNode => { if (this.state.status) { const data = this.state.controlPlaneNode!.getData() as NodeData; + return ( ; - } + return
Control plane metrics are not available
; + }; } -export const TargetPanelControlPlane = withTranslation(I18N_NAMESPACE)(TargetPanelControlPlaneComponent); +export const TargetPanelControlPlane = withKialiTranslation()(TargetPanelControlPlaneComponent); diff --git a/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx index 9e3fee1270..a526ee4fea 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx @@ -434,6 +434,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< private renderCharts(direction: DirectionType): React.ReactNode { if (this.state.status) { const namespace = this.props.targetNamespace; + return ( ; + return
Namespace metrics are not available
; } private renderStatus(): React.ReactNode { diff --git a/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx b/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx index 322fdd61d0..9c55c8ca91 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx @@ -6,8 +6,8 @@ import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/Summa import { elems, selectAnd } from '../MeshElems'; import { MeshAttr, MeshInfraType } from 'types/Mesh'; import { renderNodeHeader } from './TargetPanelNode'; -import { WithTranslation, withTranslation } from 'react-i18next'; -import { I18N_NAMESPACE } from 'types/Common'; +import { WithTranslation } from 'react-i18next'; +import { withKialiTranslation } from 'utils/I18nUtils'; type TargetPanelMeshState = { loading: boolean; @@ -66,8 +66,8 @@ class TargetPanelMeshComponent extends React.Component ( - <>{infraNodes.map(node => renderNodeHeader(node.getData(), this.props.t, true))} + <>{infraNodes.map(node => renderNodeHeader(node.getData(), true))} ); } -export const TargetPanelMesh = withTranslation(I18N_NAMESPACE)(TargetPanelMeshComponent); +export const TargetPanelMesh = withKialiTranslation()(TargetPanelMeshComponent); diff --git a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx index 46d07404d1..3c456661a4 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx @@ -152,13 +152,7 @@ export class TargetPanelNamespace extends React.Component - + {namespaceActions}, hasNoOffset: false, className: undefined }} @@ -259,11 +253,7 @@ export class TargetPanelNamespace extends React.Component { return (
- + <span className={namespaceNameStyle}> @@ -838,6 +828,7 @@ export class TargetPanelNamespace extends React.Component<TargetPanelNamespacePr private renderCharts(): React.ReactNode { if (this.state.status) { const namespace = this.state.targetNamespace!; + return ( <OverviewCardSparklineCharts key={namespace} @@ -853,7 +844,7 @@ export class TargetPanelNamespace extends React.Component<TargetPanelNamespacePr ); } - return <div style={{ height: '70px' }} />; + return <div style={{ padding: '1.5rem 0', textAlign: 'center' }}>Namespace metrics are not available</div>; } private renderStatus(): React.ReactNode { diff --git a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx index db7243d9dc..86849ab109 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx @@ -7,9 +7,8 @@ import { MeshInfraType, MeshNodeData, isExternal } from 'types/Mesh'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { Title, TitleSizes } from '@patternfly/react-core'; -import { WithTranslation, withTranslation } from 'react-i18next'; -import { I18N_NAMESPACE } from 'types/Common'; -import { TFunction } from 'react-i18next'; +import { WithTranslation } from 'react-i18next'; +import { withKialiTranslation } from 'utils/I18nUtils'; type TargetPanelNodeProps = WithTranslation & TargetPanelCommonProps; @@ -28,12 +27,7 @@ const nodeStyle = kialiStyle({ display: 'flex' }); -export function renderNodeHeader( - data: MeshNodeData, - t: TFunction, - nameOnly?: boolean, - nameSize?: TitleSizes -): React.ReactNode { +export function renderNodeHeader(data: MeshNodeData, nameOnly?: boolean, nameSize?: TitleSizes): React.ReactNode { let pfBadge; switch (data.infraType) { @@ -66,7 +60,7 @@ export function renderNodeHeader( <span className={nodeStyle}> <PFBadge badge={pfBadge} size="global" /> {data.infraName} - {getHealthStatus(data, t)} + {getHealthStatus(data)} </span> {!nameOnly && ( @@ -117,7 +111,7 @@ class TargetPanelNodeComponent extends React.Component -
{renderNodeHeader(data, this.props.t, isExternal(data.cluster))}
+
{renderNodeHeader(data, isExternal(data.cluster))}
{data.version && (
@@ -133,4 +127,4 @@ class TargetPanelNodeComponent extends React.Component = (props: Props) => { return (
-
-
- Canary upgrade status - - - -
- -
- (datum.x ? `${datum.x}: ${datum.y.toFixed(2)}%` : null)} - invert - title={`${migrated.toFixed(2)}%`} - height={170} - themeColor={ChartThemeColor.green} - /> -
- -
-

{`${props.canaryUpgradeStatus.migratedNamespaces.length} of ${total} namespaces migrated`}

-
+ Canary upgrade status + + + + + +
+ (datum.x ? `${datum.x}: ${datum.y.toFixed(2)}%` : null)} + invert + title={`${migrated.toFixed(2)}%`} + height={170} + themeColor={ChartThemeColor.green} + />
+ +

{`${props.canaryUpgradeStatus.migratedNamespaces.length} of ${total} namespaces migrated`}

); }; diff --git a/frontend/src/pages/Overview/OverviewPage.tsx b/frontend/src/pages/Overview/OverviewPage.tsx index f7eaa85a4f..9016b31217 100644 --- a/frontend/src/pages/Overview/OverviewPage.tsx +++ b/frontend/src/pages/Overview/OverviewPage.tsx @@ -70,7 +70,6 @@ import { ControlPlaneBadge } from './ControlPlaneBadge'; import { OverviewStatus } from './OverviewStatus'; import { IstiodResourceThresholds } from 'types/IstioStatus'; import { TLSInfo } from 'components/Overview/TLSInfo'; -import { CanaryUpgradeProgress } from './CanaryUpgradeProgress'; import { ControlPlaneVersionBadge } from './ControlPlaneVersionBadge'; import { AmbientBadge } from '../../components/Ambient/AmbientBadge'; import { PFBadge, PFBadges } from 'components/Pf/PfBadges'; @@ -1041,8 +1040,6 @@ export class OverviewPageComponent extends React.Component render(): React.ReactNode { const sm = this.state.displayMode === OverviewDisplayMode.COMPACT ? 3 : 6; const md = this.state.displayMode === OverviewDisplayMode.COMPACT ? 3 : 4; - const rlg = 4; - const lg = 12; const filteredNamespaces = FilterHelper.runFilters( this.state.namespaces, @@ -1085,24 +1082,8 @@ export class OverviewPageComponent extends React.Component {filteredNamespaces.map((ns, i) => { return ( isCompact={true} className={cardGridStyle} data-test={`${ns.name}-${OverviewDisplayMode[this.state.displayMode]}`} - style={ - !this.props.istioAPIEnabled && !this.hasCanaryUpgradeConfigured() ? { height: '96%' } : {} - } > !isRemoteCluster(ns.annotations) && this.state.displayMode === OverviewDisplayMode.EXPAND && ( - + {this.renderLabels(ns)}
@@ -1180,16 +1158,8 @@ export class OverviewPageComponent extends React.Component {ns.name === serverConfig.istioNamespace && ( - {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && ( - - - - )} - {this.props.istioAPIEnabled === true && ( - - {this.renderCharts(ns)} - + {this.renderCharts(ns)} )} @@ -1344,6 +1314,7 @@ export class OverviewPageComponent extends React.Component if (this.state.displayMode === OverviewDisplayMode.COMPACT) { return ; } + return ( direction={this.state.direction} metrics={ns.metrics} errorMetrics={ns.errorMetrics} - // controlPlaneMetrics={ns.controlPlaneMetrics} istiodResourceThresholds={this.state.istiodResourceThresholds} /> ); } - return
; + return
Namespace metrics are not available
; }; renderIstioConfigStatus = (ns: NamespaceInfo): React.ReactNode => { diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 508c8ceb59..43546d12a8 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -15,7 +15,7 @@ import { IstioConfigDetailsRoute } from 'routes/IstioConfigDetailsRoute'; import { IstioConfigNewRoute } from 'routes/IstioConfigNewRoute'; import { GraphRoutePF } from 'routes/GraphRoutePF'; import { GraphPagePF } from 'pages/GraphPF/GraphPagePF'; -import { i18n } from 'i18n'; +import { t } from 'utils/I18nUtils'; /** * Return array of objects that describe vertical menu @@ -24,54 +24,54 @@ import { i18n } from 'i18n'; const navMenuItems: MenuItem[] = [ { id: 'overview', - title: i18n.t('Overview'), + title: t('Overview'), to: '/overview', pathsActive: [/^\/overview\/(.*)/] }, { id: 'traffic_graph_cy', - title: i18n.t('Traffic Graph [Cy]'), + title: t('Traffic Graph [Cy]'), to: '/graph/namespaces/', pathsActive: [/^\/graph\/(.*)/] }, { id: 'traffic_graph_pf', - title: i18n.t('Traffic Graph [PF]'), + title: t('Traffic Graph [PF]'), to: '/graphpf/namespaces/', pathsActive: [/^\/graphpf\/(.*)/] }, { id: 'applications', - title: i18n.t('Applications'), + title: t('Applications'), to: `/${Paths.APPLICATIONS}`, pathsActive: [new RegExp(`^/namespaces/(.*)/${Paths.APPLICATIONS}/(.*)`)] }, { id: 'workloads', - title: i18n.t('Workloads'), + title: t('Workloads'), to: `/${Paths.WORKLOADS}`, pathsActive: [new RegExp(`^/namespaces/(.*)/${Paths.WORKLOADS}/(.*)`)] }, { id: 'services', - title: i18n.t('Services'), + title: t('Services'), to: `/${Paths.SERVICES}`, pathsActive: [new RegExp(`^/namespaces/(.*)/${Paths.SERVICES}/(.*)`)] }, { id: 'istio', - title: i18n.t('Istio Config'), + title: t('Istio Config'), to: `/${Paths.ISTIO}`, pathsActive: [new RegExp(`^/namespaces/(.*)/${Paths.ISTIO}/(.*)`), new RegExp(`/${Paths.ISTIO}/new/(.*)`)] }, { id: 'tracing', - title: i18n.t('Distributed Tracing'), + title: t('Distributed Tracing'), to: '/tracing' }, { id: 'mesh', - title: i18n.t('Mesh'), + title: t('Mesh'), to: '/mesh' } ]; diff --git a/frontend/src/utils/I18nUtils.ts b/frontend/src/utils/I18nUtils.ts new file mode 100644 index 0000000000..b418e34ccb --- /dev/null +++ b/frontend/src/utils/I18nUtils.ts @@ -0,0 +1,23 @@ +import { TOptions } from 'i18next'; +import { UseTranslationResponse, getI18n, useTranslation, withTranslation } from 'react-i18next'; +import { I18N_NAMESPACE } from 'types/Common'; + +/* eslint-disable @typescript-eslint/explicit-function-return-type*/ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types*/ +export const withKialiTranslation = () => { + return withTranslation(I18N_NAMESPACE); +}; + +/** + * A Hook for using the i18n translation. + */ +export const useKialiTranslation = (): UseTranslationResponse => { + return useTranslation(I18N_NAMESPACE); +}; + +/** + * a function to perform translation to I18_NAMESPACE namespace + * @param value string to translate + * @param options (optional) options for traslations + */ +export const t = (value: string, options?: TOptions): string => getI18n().t(value, { ns: I18N_NAMESPACE, ...options }); diff --git a/frontend/src/utils/ThemeUtils.ts b/frontend/src/utils/ThemeUtils.ts index b31a8eabf2..333e7ad533 100644 --- a/frontend/src/utils/ThemeUtils.ts +++ b/frontend/src/utils/ThemeUtils.ts @@ -1,3 +1,4 @@ +import { useKialiSelector } from 'hooks/redux'; import { store } from 'store/ConfigStore'; import { Theme } from 'types/Common'; @@ -5,6 +6,10 @@ export const getKialiTheme = (): Theme => { return (store.getState().globalState.theme as Theme) || getDefaultTheme(); }; +export const useKialiTheme = (): string => { + return useKialiSelector(state => state.globalState.theme) || getDefaultTheme(); +}; + // Get default theme from system settings const getDefaultTheme = (): Theme => { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { From 668c2de196a41770591530f6e1cb017d17d94915 Mon Sep 17 00:00:00 2001 From: Fernando Hoyos Date: Thu, 9 May 2024 11:54:34 +0200 Subject: [PATCH 28/46] Simplify i18n translation in target panel --- .../pages/Mesh/target/TargetPanelCluster.tsx | 23 +++++++------------ .../Mesh/target/TargetPanelControlPlane.tsx | 15 ++++-------- .../src/pages/Mesh/target/TargetPanelMesh.tsx | 8 ++----- .../Mesh/target/TargetPanelNamespace.tsx | 17 +++++++++----- .../src/pages/Mesh/target/TargetPanelNode.tsx | 8 ++----- frontend/src/utils/I18nUtils.ts | 8 +------ 6 files changed, 29 insertions(+), 50 deletions(-) diff --git a/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx b/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx index 9b36974814..281eb1bc6e 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelCluster.tsx @@ -17,11 +17,10 @@ import { TitleSizes, Tooltip } from '@patternfly/react-core'; import { classes } from 'typestyle'; import { descendents } from '../MeshElems'; import { renderNodeHeader } from './TargetPanelNode'; -import { WithTranslation } from 'react-i18next'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; -import { withKialiTranslation } from 'utils/I18nUtils'; +import { t } from 'utils/I18nUtils'; -type TargetPanelClusterProps = WithTranslation & TargetPanelCommonProps; +type TargetPanelClusterProps = TargetPanelCommonProps; type TargetPanelClusterState = { clusterNode?: Node; @@ -38,7 +37,7 @@ const kialiIconStyle = kialiStyle({ marginRight: '0.25rem' }); -class TargetPanelClusterComponent extends React.Component { +export class TargetPanelCluster extends React.Component { static readonly panelStyle = { backgroundColor: PFColors.BackgroundColor100, height: '100%', @@ -98,7 +97,7 @@ class TargetPanelClusterComponent extends React.Component
{clusterData.isKialiHome && ( - + )} @@ -122,19 +121,15 @@ class TargetPanelClusterComponent extends React.Component - {`${this.props.t('Version')}: `} - {version} + {`${t('Version')}: ${version}`}
)} - {`${this.props.t('Network')}: `} - {clusterData.network ? clusterData.network : 'n/a'} + {`${t('Network')}: ${clusterData.network || t('n/a')}`}
- {`${this.props.t('API Endpoint')}: `} - {clusterData.apiEndpoint ? clusterData.apiEndpoint : 'n/a'} + {`${t('API Endpoint')}: ${clusterData.apiEndpoint || t('n/a')}`}
- {`${this.props.t('Secret Name')}: `} - {clusterData.secretName ? clusterData.secretName : 'n/a'} + {`${t('Secret Name')}: ${clusterData.secretName || t('n/a')}`}
)}
@@ -191,5 +186,3 @@ class TargetPanelClusterComponent extends React.Component { @@ -458,5 +455,3 @@ class TargetPanelControlPlaneComponent extends React.Component< return
Control plane metrics are not available
; }; } - -export const TargetPanelControlPlane = withKialiTranslation()(TargetPanelControlPlaneComponent); diff --git a/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx b/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx index 9c55c8ca91..df352eaba5 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelMesh.tsx @@ -6,8 +6,6 @@ import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/Summa import { elems, selectAnd } from '../MeshElems'; import { MeshAttr, MeshInfraType } from 'types/Mesh'; import { renderNodeHeader } from './TargetPanelNode'; -import { WithTranslation } from 'react-i18next'; -import { withKialiTranslation } from 'utils/I18nUtils'; type TargetPanelMeshState = { loading: boolean; @@ -19,9 +17,9 @@ const defaultState: TargetPanelMeshState = { loading: false }; -type TargetPanelMeshProps = WithTranslation & TargetPanelCommonProps; +type TargetPanelMeshProps = TargetPanelCommonProps; -class TargetPanelMeshComponent extends React.Component { +export class TargetPanelMesh extends React.Component { constructor(props: TargetPanelMeshProps) { super(props); @@ -69,5 +67,3 @@ class TargetPanelMeshComponent extends React.Component{infraNodes.map(node => renderNodeHeader(node.getData(), true))} ); } - -export const TargetPanelMesh = withKialiTranslation()(TargetPanelMeshComponent); diff --git a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx index 3c456661a4..519e44a089 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNamespace.tsx @@ -187,16 +187,21 @@ export class TargetPanelNamespace extends React.Component - {targetPanelHR} + <> {this.state.canaryUpgradeStatus && this.hasCanaryUpgradeConfigured() && ( -
+ <> {targetPanelHR} -
+ )} -
{this.props.istioAPIEnabled &&
{this.renderCharts()}
}
-
+ + {this.props.istioAPIEnabled && ( + <> + {targetPanelHR} + {this.renderCharts()} + + )} + )} )} diff --git a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx index 86849ab109..d73756ae5b 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx @@ -7,10 +7,8 @@ import { MeshInfraType, MeshNodeData, isExternal } from 'types/Mesh'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { Title, TitleSizes } from '@patternfly/react-core'; -import { WithTranslation } from 'react-i18next'; -import { withKialiTranslation } from 'utils/I18nUtils'; -type TargetPanelNodeProps = WithTranslation & TargetPanelCommonProps; +type TargetPanelNodeProps = TargetPanelCommonProps; type TargetPanelNodeState = { loading: boolean; @@ -79,7 +77,7 @@ export function renderNodeHeader(data: MeshNodeData, nameOnly?: boolean, nameSiz ); } -class TargetPanelNodeComponent extends React.Component { +export class TargetPanelNode extends React.Component { constructor(props: TargetPanelNodeProps) { super(props); @@ -126,5 +124,3 @@ class TargetPanelNodeComponent extends React.Component { - return withTranslation(I18N_NAMESPACE); -}; - /** * A Hook for using the i18n translation. */ From d47b4706869a541365924151e290ffcfd2722237 Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Thu, 9 May 2024 14:17:19 -0400 Subject: [PATCH 29/46] Fix yarn unit test fails --- .../MTls/__tests__/MTLSIcon.test.tsx | 52 ++++--- .../__snapshots__/MTLSIcon.test.tsx.snap | 133 ++++++++++++++++-- frontend/src/setupTests.ts | 5 + 3 files changed, 157 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx b/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx index b1aa5deaf2..a7d91092bf 100644 --- a/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx +++ b/frontend/src/components/MTls/__tests__/MTLSIcon.test.tsx @@ -1,42 +1,50 @@ import * as React from 'react'; -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; import { MTLSIcon, MTLSIconTypes } from '../MTLSIcon'; -import { shallowToJson } from 'enzyme-to-json'; -import { TooltipPosition } from '@patternfly/react-core'; +import { mountToJson } from 'enzyme-to-json'; +import { Tooltip, TooltipPosition } from '@patternfly/react-core'; +import { Provider } from 'react-redux'; +import { store } from 'store/ConfigStore'; const mockIcon = (icon: string) => { const component = ( - + + + ); - return shallow(component); + return mount(component); }; describe('when Icon is LOCK_FULL', () => { it('MTLSIcon renders properly', () => { - const wrapper = mockIcon(MTLSIconTypes.LOCK_FULL); + const mount = mockIcon(MTLSIconTypes.LOCK_FULL); - expect(shallowToJson(wrapper)).toBeDefined(); - expect(shallowToJson(wrapper)).toMatchSnapshot(); + expect(mountToJson(mount)).toBeDefined(); + expect(mountToJson(mount)).toMatchSnapshot(); - expect(wrapper.name()).toEqual('Tooltip'); - expect(wrapper.props().position).toEqual('right'); - expect(wrapper.props().content).toEqual('Overlay Test'); + const tooltip = mount.find(Tooltip); + expect(tooltip.exists()).toBeTruthy(); + expect(tooltip.props().position).toEqual('right'); + expect(tooltip.props().content).toEqual('Overlay Test'); - expect(wrapper.children()).toBeDefined(); - expect(wrapper.children().name()).toEqual('img'); - expect(wrapper.children().prop('className')).toEqual('className'); - expect(wrapper.children().prop('src')).toEqual('mtls-status-full.svg'); + const img = tooltip.find('img'); + expect(img.exists()).toBeTruthy(); + expect(img.props().className).toEqual('className'); + expect(img.props().src).toEqual('mtls-status-full-dark.svg'); }); }); describe('when Icon is LOCK_HOLLOW', () => { it('MTLSIcon renders properly', () => { - const wrapper = mockIcon(MTLSIconTypes.LOCK_HOLLOW); - expect(wrapper.children().prop('src')).toEqual('mtls-status-partial.svg'); + const mount = mockIcon(MTLSIconTypes.LOCK_HOLLOW); + const img = mount.find('img'); + expect(img.exists()).toBeTruthy(); + expect(img.props().className).toEqual('className'); + expect(img.props().src).toEqual('mtls-status-partial-dark.svg'); }); }); diff --git a/frontend/src/components/MTls/__tests__/__snapshots__/MTLSIcon.test.tsx.snap b/frontend/src/components/MTls/__tests__/__snapshots__/MTLSIcon.test.tsx.snap index 6f91542857..2457fe80ca 100644 --- a/frontend/src/components/MTls/__tests__/__snapshots__/MTLSIcon.test.tsx.snap +++ b/frontend/src/components/MTls/__tests__/__snapshots__/MTLSIcon.test.tsx.snap @@ -1,16 +1,127 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`when Icon is LOCK_FULL MTLSIcon renders properly 1`] = ` - - right - + + + + + + Overlay Test + +
+ } + popperRef={ + Object { + "current": null, + } + } + positionModifiers={ + Object { + "bottom": "pf-m-bottom", + "bottom-end": "pf-m-bottom-right", + "bottom-start": "pf-m-bottom-left", + "left": "pf-m-left", + "left-end": "pf-m-left-bottom", + "left-start": "pf-m-left-top", + "right": "pf-m-right", + "right-end": "pf-m-right-bottom", + "right-start": "pf-m-right-top", + "top": "pf-m-top", + "top-end": "pf-m-top-right", + "top-start": "pf-m-top-left", + } + } + trigger={ + right + } + zIndex={9999} + > +
+ right +
+ + + + `; diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index a1e2ca2e10..d66461a108 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -31,5 +31,10 @@ jest.mock('react-i18next', () => ({ withTranslation: () => (component: any) => { component.defaultProps = { ...component.defaultProps, t: (key: string) => key }; return component; + }, + getI18n: () => { + return { + t: (key: string) => key + }; } })); From 632bc6899c79ef59f3bf1f1a12022b38c44391cb Mon Sep 17 00:00:00 2001 From: Jay Shaughnessy Date: Thu, 9 May 2024 14:51:15 -0400 Subject: [PATCH 30/46] Make Infra IDs more unique by incorporating version. This solves an issue with DataPlane nodes for canary versions, and possibly other situations. --- frontend/public/locales/zh/translation.json | 1 + mesh/api/testdata/test_mesh_graph.expected | 164 ++++++++++---------- mesh/config/cytoscape/cytoscape.go | 2 +- mesh/generator/generator.go | 4 +- mesh/types.go | 4 +- 5 files changed, 88 insertions(+), 87 deletions(-) diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json index 78116de5e3..b1ea76889e 100644 --- a/frontend/public/locales/zh/translation.json +++ b/frontend/public/locales/zh/translation.json @@ -67,6 +67,7 @@ "Min TLS version": "最低TLS版本", "More info at": "More info at", "mTLS": "mTLS", + "n/a": "n/a", "Name": "名称", "Namespace": "命名空间", "Namespace Label": "命名空间标签", diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected index 339e5d7532..1ce2579bfb 100644 --- a/mesh/api/testdata/test_mesh_graph.expected +++ b/mesh/api/testdata/test_mesh_graph.expected @@ -3,7 +3,7 @@ "nodes": [ { "data": { - "id": "f2a39c06544bab3ee6f6b9014db8530d", + "id": "107648411a3f61763d45f8433b787970", "cluster": "_external_", "infraName": "External Deployments", "infraType": "cluster", @@ -17,7 +17,7 @@ }, { "data": { - "id": "8b0417cc2584b04a88544ddcb1c18174", + "id": "63477ebaf31eccdd960ec1d9ff35b478", "cluster": "cluster-primary", "infraName": "cluster-primary", "infraType": "cluster", @@ -46,7 +46,7 @@ }, { "data": { - "id": "ecbd12dbd745b071361a4fbb12f3ebe8", + "id": "1aabe556f7e14438273ef43c7bce6148", "cluster": "cluster-remote", "infraName": "cluster-remote", "infraType": "cluster", @@ -67,8 +67,8 @@ }, { "data": { - "id": "54de7d83a35cb05d3834c1b53d7c53e6", - "parent": "8b0417cc2584b04a88544ddcb1c18174", + "id": "d1c7a41aefa12a640ebeb0e57c079e8e", + "parent": "63477ebaf31eccdd960ec1d9ff35b478", "cluster": "cluster-primary", "infraName": "istio-system", "infraType": "namespace", @@ -80,8 +80,8 @@ }, { "data": { - "id": "0ff3498362503e275d0d47f0e6ca5479", - "parent": "f2a39c06544bab3ee6f6b9014db8530d", + "id": "82397758134f81118fb9935477d6f598", + "parent": "107648411a3f61763d45f8433b787970", "cluster": "_external_", "infraName": "Prometheus", "infraType": "metricStore", @@ -118,11 +118,11 @@ }, { "data": { - "id": "726a5e87be54c6dfbbc55b22e3cbb1c5", - "parent": "f2a39c06544bab3ee6f6b9014db8530d", + "id": "9c46b0cb8c955460035427187706cfbc", + "parent": "107648411a3f61763d45f8433b787970", "cluster": "_external_", - "infraName": "Grafana", - "infraType": "grafana", + "infraName": "jaeger", + "infraType": "traceStore", "namespace": "", "nodeType": "infra", "healthData": "Healthy", @@ -136,12 +136,22 @@ "UseKialiToken": false, "Username": "xxx" }, - "Dashboards": null, "Enabled": true, "HealthCheckUrl": "", - "InClusterURL": "http://grafana.istio-system:3000", + "GrpcPort": 9095, + "InClusterURL": "http://tracing.istio-system:16685/jaeger", "IsCore": false, - "URL": "" + "Provider": "jaeger", + "TempoConfig": {}, + "NamespaceSelector": true, + "QueryScope": {}, + "QueryTimeout": 5, + "URL": "", + "UseGRPC": true, + "WhiteListIstioSystem": [ + "jaeger-query", + "istio-ingressgateway" + ] }, "isExternal": true, "isInaccessible": true @@ -149,11 +159,11 @@ }, { "data": { - "id": "e4dddd6aa55b9806c0e99646d0ac4711", - "parent": "f2a39c06544bab3ee6f6b9014db8530d", + "id": "fb536b180952008dd29b4319593ef044", + "parent": "107648411a3f61763d45f8433b787970", "cluster": "_external_", - "infraName": "jaeger", - "infraType": "traceStore", + "infraName": "Grafana", + "infraType": "grafana", "namespace": "", "nodeType": "infra", "healthData": "Healthy", @@ -167,22 +177,12 @@ "UseKialiToken": false, "Username": "xxx" }, + "Dashboards": null, "Enabled": true, "HealthCheckUrl": "", - "GrpcPort": 9095, - "InClusterURL": "http://tracing.istio-system:16685/jaeger", + "InClusterURL": "http://grafana.istio-system:3000", "IsCore": false, - "Provider": "jaeger", - "TempoConfig": {}, - "NamespaceSelector": true, - "QueryScope": {}, - "QueryTimeout": 5, - "URL": "", - "UseGRPC": true, - "WhiteListIstioSystem": [ - "jaeger-query", - "istio-ingressgateway" - ] + "URL": "" }, "isExternal": true, "isInaccessible": true @@ -190,8 +190,8 @@ }, { "data": { - "id": "7c9269b87249746045770ea45dec2786", - "parent": "8b0417cc2584b04a88544ddcb1c18174", + "id": "a1c47185abeae268a8280338027e20a5", + "parent": "63477ebaf31eccdd960ec1d9ff35b478", "cluster": "cluster-primary", "infraName": "Data Plane", "infraType": "dataplane", @@ -218,35 +218,8 @@ }, { "data": { - "id": "b4fef251c5e835d9269751857825b5ea", - "parent": "54de7d83a35cb05d3834c1b53d7c53e6", - "cluster": "cluster-primary", - "infraName": "istiod", - "infraType": "istiod", - "namespace": "istio-system", - "nodeType": "infra", - "healthData": "Healthy", - "infraData": { - "OutboundTrafficPolicy": { - "mode": "" - }, - "Network": "", - "DisableMixerHttpReports": false, - "DiscoverySelectors": null, - "EnableAutoMtls": true, - "MeshMTLS": { - "MinProtocolVersion": "" - }, - "defaultConfig": { - "MeshId": "" - } - } - } - }, - { - "data": { - "id": "ed31cec8e279d017c1ac1f7100723e79", - "parent": "54de7d83a35cb05d3834c1b53d7c53e6", + "id": "cdf7185cca90872db8b743e7a5b36ef0", + "parent": "d1c7a41aefa12a640ebeb0e57c079e8e", "cluster": "cluster-primary", "infraName": "kiali", "infraType": "kiali", @@ -303,8 +276,35 @@ }, { "data": { - "id": "f50226d64063fb859a1d7fad6a609978", - "parent": "ecbd12dbd745b071361a4fbb12f3ebe8", + "id": "e0de428cc2c4c6381c3f8082a615f25d", + "parent": "d1c7a41aefa12a640ebeb0e57c079e8e", + "cluster": "cluster-primary", + "infraName": "istiod", + "infraType": "istiod", + "namespace": "istio-system", + "nodeType": "infra", + "healthData": "Healthy", + "infraData": { + "OutboundTrafficPolicy": { + "mode": "" + }, + "Network": "", + "DisableMixerHttpReports": false, + "DiscoverySelectors": null, + "EnableAutoMtls": true, + "MeshMTLS": { + "MinProtocolVersion": "" + }, + "defaultConfig": { + "MeshId": "" + } + } + } + }, + { + "data": { + "id": "6514e7140e9a0c07ca296b90cf22cf9f", + "parent": "1aabe556f7e14438273ef43c7bce6148", "cluster": "cluster-remote", "infraName": "Data Plane", "infraType": "dataplane", @@ -333,44 +333,44 @@ "edges": [ { "data": { - "id": "c33dfb69aa02feb7e0798c9185a68525", - "source": "b4fef251c5e835d9269751857825b5ea", - "target": "7c9269b87249746045770ea45dec2786" + "id": "1425e29b8977c19c906115f31a1f8a6b", + "source": "cdf7185cca90872db8b743e7a5b36ef0", + "target": "82397758134f81118fb9935477d6f598" } }, { "data": { - "id": "758f05601a173df0bed8693eaf5cfbb6", - "source": "b4fef251c5e835d9269751857825b5ea", - "target": "f50226d64063fb859a1d7fad6a609978" + "id": "d1d2a29141584c3aebd0c2940e951030", + "source": "cdf7185cca90872db8b743e7a5b36ef0", + "target": "9c46b0cb8c955460035427187706cfbc" } }, { "data": { - "id": "574ebfd667eaa055aeefdec6d46cd012", - "source": "ed31cec8e279d017c1ac1f7100723e79", - "target": "0ff3498362503e275d0d47f0e6ca5479" + "id": "8e54c055e5da60e7a310ab5f8bc0aa3d", + "source": "cdf7185cca90872db8b743e7a5b36ef0", + "target": "e0de428cc2c4c6381c3f8082a615f25d" } }, { "data": { - "id": "4f46af89fe1178d9847cec0dde5ea9da", - "source": "ed31cec8e279d017c1ac1f7100723e79", - "target": "726a5e87be54c6dfbbc55b22e3cbb1c5" + "id": "e44f9ff10bbd28f2c0c2b3f9d6b442f1", + "source": "cdf7185cca90872db8b743e7a5b36ef0", + "target": "fb536b180952008dd29b4319593ef044" } }, { "data": { - "id": "b1314f35029eaa664b52b865540e1427", - "source": "ed31cec8e279d017c1ac1f7100723e79", - "target": "b4fef251c5e835d9269751857825b5ea" + "id": "988fc84000271532ec53aac83a7a8b39", + "source": "e0de428cc2c4c6381c3f8082a615f25d", + "target": "6514e7140e9a0c07ca296b90cf22cf9f" } }, { "data": { - "id": "7410fb5a927072cb1547d44b61e104ec", - "source": "ed31cec8e279d017c1ac1f7100723e79", - "target": "e4dddd6aa55b9806c0e99646d0ac4711" + "id": "5d4c6712e449af9af6ce0719cd42eda8", + "source": "e0de428cc2c4c6381c3f8082a615f25d", + "target": "a1c47185abeae268a8280338027e20a5" } } ] diff --git a/mesh/config/cytoscape/cytoscape.go b/mesh/config/cytoscape/cytoscape.go index 5bef497e32..24b68d7769 100644 --- a/mesh/config/cytoscape/cytoscape.go +++ b/mesh/config/cytoscape/cytoscape.go @@ -210,7 +210,7 @@ func boxByNamespace(nodes *[]*NodeWrapper) { continue } - id, err := mesh.Id(nd.Cluster, nd.Namespace, nd.Namespace, nd.InfraType, false) + id, err := mesh.Id(nd.Cluster, nd.Namespace, nd.Namespace, nd.InfraType, "", false) mesh.CheckError(err) box, found := namespaceBoxes[id] diff --git a/mesh/generator/generator.go b/mesh/generator/generator.go index feeefb79db..bcae3de3fb 100644 --- a/mesh/generator/generator.go +++ b/mesh/generator/generator.go @@ -141,7 +141,7 @@ func BuildMeshMap(ctx context.Context, o mesh.Options, gi *mesh.AppenderGlobalIn return cmp.Compare(a.Name, b.Name) }) - dp, _, err := addInfra(meshMap, mesh.InfraTypeDataPlane, cluster, "", "Data Plane", namespaces, "", false, "") + dp, _, err := addInfra(meshMap, mesh.InfraTypeDataPlane, cluster, "", "Data Plane", namespaces, cp.Revision, false, "") graph.CheckError(err) istiod.AddEdge(dp) @@ -208,7 +208,7 @@ func BuildMeshMap(ctx context.Context, o mesh.Options, gi *mesh.AppenderGlobalIn } func addInfra(meshMap mesh.MeshMap, infraType, cluster, namespace, name string, infraData interface{}, version string, isExternal bool, healthData string) (*mesh.Node, bool, error) { - id, err := mesh.Id(cluster, namespace, name, infraType, isExternal) + id, err := mesh.Id(cluster, namespace, name, infraType, version, isExternal) if err != nil { return nil, false, err } diff --git a/mesh/types.go b/mesh/types.go index def4488e73..14acace703 100644 --- a/mesh/types.go +++ b/mesh/types.go @@ -104,9 +104,9 @@ func NewMeshMap() MeshMap { } // Id returns the unique node ID -func Id(cluster, namespace, name, infraType string, isExternal bool) (id string, err error) { +func Id(cluster, namespace, name, infraType, version string, isExternal bool) (id string, err error) { if cluster == "" || (namespace == "" && !(isExternal || infraType == InfraTypeCluster || infraType == InfraTypeDataPlane)) || name == "" { return "", fmt.Errorf("failed Mesh ID gen: type=[%s] cluster=[%s] namespace=[%s] name=[%s], isExternal=[%v]", infraType, cluster, namespace, name, isExternal) } - return fmt.Sprintf("infra_%s_%s_%s", cluster, namespace, name), nil + return fmt.Sprintf("infra_%s_%s_%s_%s", cluster, namespace, name, version), nil } From 6bf67c02cc469d6f1f3b5119095a892100c8b157 Mon Sep 17 00:00:00 2001 From: Fernando Hoyos Date: Fri, 10 May 2024 19:14:48 +0200 Subject: [PATCH 31/46] Add canary badge to data plane --- frontend/public/locales/zh/translation.json | 4 + .../components/IstioStatus/IstioStatus.tsx | 169 ++++++++-------- .../IstioStatus/IstioStatusInline.tsx | 26 --- .../__tests__/IstioStatus.test.tsx | 6 +- .../__snapshots__/IstioStatus.test.tsx.snap | 16 +- .../src/components/Nav/Masthead/Masthead.tsx | 2 +- .../Mesh/target/TargetPanelControlPlane.tsx | 7 +- .../Mesh/target/TargetPanelDataPlane.tsx | 182 ++++++++---------- .../target/TargetPanelDataPlaneNamespace.tsx | 36 ++-- .../src/pages/Mesh/target/TargetPanelNode.tsx | 10 +- .../src/pages/Overview/ControlPlaneBadge.tsx | 39 +++- .../Overview/ControlPlaneVersionBadge.tsx | 30 ++- .../__tests__/ControlPlaneBadge.test.tsx | 2 +- frontend/src/setupTests.ts | 20 ++ frontend/src/types/Mesh.ts | 20 +- mesh/config/cytoscape/cytoscape.go | 5 + mesh/generator/generator.go | 39 ++-- mesh/meta.go | 1 + 18 files changed, 324 insertions(+), 290 deletions(-) delete mode 100644 frontend/src/components/IstioStatus/IstioStatusInline.tsx diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json index b1ea76889e..f04c418742 100644 --- a/frontend/public/locales/zh/translation.json +++ b/frontend/public/locales/zh/translation.json @@ -11,6 +11,7 @@ "Cluster": "集群", "Compact view": "紧凑视图", "Concentric": "Concentric", + "Configuration": "Configuration", "Control plane": "控制平面", "Control plane metrics": "控制平面指标", "cores": "cores", @@ -54,11 +55,14 @@ "Istio config": "Istio配置", "Istio Config": "Istio配置", "Istio deployment status disabled.": "Istio deployment status disabled.", + "It belongs to the istio control plane": "It belongs to the istio control plane", + "It belongs to the istio revision": "It belongs to the istio revision", "Kiali home cluster": "Kiali home cluster", "Kiali home cluster: {{name}}": "Kiali主集群: {{name}}", "Kiali on GitHub": "Kiali on GitHub", "Last": "最近的", "List view": "列表视图", + "Loading...": "Loading...", "mb": "mb", "Mb (Threshold)": "Mb (Threshold)", "Memory": "内存", diff --git a/frontend/src/components/IstioStatus/IstioStatus.tsx b/frontend/src/components/IstioStatus/IstioStatus.tsx index a5584f9f06..62d20bb6dd 100644 --- a/frontend/src/components/IstioStatus/IstioStatus.tsx +++ b/frontend/src/components/IstioStatus/IstioStatus.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { SVGIconProps } from '@patternfly/react-icons/dist/js/createIcon'; import * as API from '../../services/Api'; import * as AlertUtils from '../../utils/AlertUtils'; -import { I18N_NAMESPACE, TimeInMilliseconds } from '../../types/Common'; +import { TimeInMilliseconds } from '../../types/Common'; import { ComponentStatus, Status } from '../../types/IstioStatus'; import { MessageType } from '../../types/MessageCenter'; import { Namespace } from '../../types/Namespace'; @@ -21,7 +21,7 @@ import { connectRefresh } from '../Refresh/connectRefresh'; import { kialiStyle } from 'styles/StyleUtils'; import { IconProps, createIcon } from 'config/KialiIcon'; import { Link } from 'react-router-dom'; -import { WithTranslation, withTranslation } from 'react-i18next'; +import { useKialiTranslation } from 'utils/I18nUtils'; type ReduxStateProps = { namespaces?: Namespace[]; @@ -41,11 +41,11 @@ type StatusIcons = { }; type Props = ReduxStateProps & - ReduxDispatchProps & - WithTranslation & { + ReduxDispatchProps & { cluster?: string; icons?: StatusIcons; lastRefreshAt: TimeInMilliseconds; + location?: string; }; const ValidToColor = { @@ -71,7 +71,7 @@ const iconStyle = kialiStyle({ verticalAlign: '-0.125rem' }); -const meshLinkStyle = kialiStyle({ +export const meshLinkStyle = kialiStyle({ display: 'flex', justifyContent: 'center', marginTop: '0.75rem', @@ -82,59 +82,60 @@ const meshLinkStyle = kialiStyle({ } }); -export class IstioStatusComponent extends React.Component { - componentDidMount(): void { - this.props.refreshNamespaces(); - this.fetchStatus(); - } - - componentDidUpdate(prevProps: Readonly): void { - if (this.props.lastRefreshAt !== prevProps.lastRefreshAt) { - this.fetchStatus(); - } - } - - fetchStatus = (): void => { - API.getIstioStatus(this.props.cluster) - .then(response => { - this.props.setIstioStatus(response.data); - }) - .catch(error => { - // User without namespaces can't have access to mTLS information. Reduce severity to info. - const informative = this.props.namespaces && this.props.namespaces.length < 1; - - if (informative) { - AlertUtils.addError(this.props.t('Istio deployment status disabled.'), error, 'default', MessageType.INFO); - } else { - AlertUtils.addError( - this.props.t('Error fetching Istio deployment status.'), - error, - 'default', - MessageType.ERROR - ); - } - }); - }; - - tooltipContent = (): React.ReactNode => { +export const IstioStatusComponent: React.FC = (props: Props) => { + const { t } = useKialiTranslation(); + + const { cluster, namespaces, setIstioStatus, refreshNamespaces, lastRefreshAt } = props; + + React.useEffect(() => { + refreshNamespaces(); + }, [refreshNamespaces]); + + const fetchStatus = React.useCallback( + () => (): void => { + API.getIstioStatus(cluster) + .then(response => { + setIstioStatus(response.data); + }) + .catch(error => { + // User without namespaces can't have access to mTLS information. Reduce severity to info. + const informative = namespaces && namespaces.length < 1; + + if (informative) { + AlertUtils.addError(t('Istio deployment status disabled.'), error, 'default', MessageType.INFO); + } else { + AlertUtils.addError(t('Error fetching Istio deployment status.'), error, 'default', MessageType.ERROR); + } + }); + }, + [cluster, namespaces, setIstioStatus, t] + ); + + React.useEffect(() => { + fetchStatus(); + }, [lastRefreshAt, fetchStatus]); + + const tooltipContent = (): React.ReactNode => { return ( <> - -
- {this.props.t('More info at')} - {this.props.t('Mesh page')} -
+ + {!props.location?.endsWith('/mesh') && ( +
+ {t('More info at')} + {t('Mesh page')} +
+ )} ); }; - tooltipColor = (): string => { + const tooltipColor = (): string => { let coreUnhealthy = false; let addonUnhealthy = false; let notReady = false; - Object.keys(this.props.status ?? {}).forEach((compKey: string) => { - const { status, is_core } = this.props.status[compKey]; + Object.keys(props.status ?? {}).forEach((compKey: string) => { + const { status, is_core } = props.status[compKey]; const isNotReady: boolean = status === Status.NotReady; const isUnhealthy: boolean = status !== Status.Healthy && !isNotReady; @@ -150,48 +151,46 @@ export class IstioStatusComponent extends React.Component { return ValidToColor[`${coreUnhealthy}-${addonUnhealthy}-${notReady}`]; }; - healthyComponents = (): boolean => { - return this.props.status.reduce((healthy: boolean, compStatus: ComponentStatus) => { + const healthyComponents = (): boolean => { + return props.status.reduce((healthy: boolean, compStatus: ComponentStatus) => { return healthy && compStatus.status === Status.Healthy; }, true); }; - render(): React.ReactNode { - if (!this.healthyComponents()) { - const icons = this.props.icons ? { ...defaultIcons, ...this.props.icons } : defaultIcons; - const iconColor = this.tooltipColor(); - let icon = ResourcesFullIcon; - let dataTest = 'istio-status'; - - if (iconColor === PFColors.Danger) { - icon = icons.ErrorIcon; - dataTest = `${dataTest}-danger`; - } else if (iconColor === PFColors.Warning) { - icon = icons.WarningIcon; - dataTest = `${dataTest}-warning`; - } else if (iconColor === PFColors.Info) { - icon = icons.InfoIcon; - dataTest = `${dataTest}-info`; - } else if (iconColor === PFColors.Success) { - icon = icons.HealthyIcon; - dataTest = `${dataTest}-success`; - } - - const iconProps: IconProps = { - className: iconStyle, - dataTest: dataTest - }; - - return ( - - {createIcon(iconProps, icon, iconColor)} - - ); + if (!healthyComponents()) { + const icons = props.icons ? { ...defaultIcons, ...props.icons } : defaultIcons; + const iconColor = tooltipColor(); + let icon = ResourcesFullIcon; + let dataTest = 'istio-status'; + + if (iconColor === PFColors.Danger) { + icon = icons.ErrorIcon; + dataTest = `${dataTest}-danger`; + } else if (iconColor === PFColors.Warning) { + icon = icons.WarningIcon; + dataTest = `${dataTest}-warning`; + } else if (iconColor === PFColors.Info) { + icon = icons.InfoIcon; + dataTest = `${dataTest}-info`; + } else if (iconColor === PFColors.Success) { + icon = icons.HealthyIcon; + dataTest = `${dataTest}-success`; } - return null; + const iconProps: IconProps = { + className: iconStyle, + dataTest: dataTest + }; + + return ( + + {createIcon(iconProps, icon, iconColor)} + + ); } -} + + return null; +}; const mapStateToProps = (state: KialiAppState): ReduxStateProps => ({ status: istioStatusSelector(state), @@ -205,6 +204,4 @@ const mapDispatchToProps = (dispatch: KialiDispatch): ReduxDispatchProps => ({ } }); -export const IstioStatus = connectRefresh( - connect(mapStateToProps, mapDispatchToProps)(withTranslation(I18N_NAMESPACE)(IstioStatusComponent)) -); +export const IstioStatus = connectRefresh(connect(mapStateToProps, mapDispatchToProps)(IstioStatusComponent)); diff --git a/frontend/src/components/IstioStatus/IstioStatusInline.tsx b/frontend/src/components/IstioStatus/IstioStatusInline.tsx deleted file mode 100644 index 5498563e97..0000000000 --- a/frontend/src/components/IstioStatus/IstioStatusInline.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import { IstioStatus } from './IstioStatus'; -import { - CheckCircleIcon, - ExclamationCircleIcon, - ExclamationTriangleIcon, - MinusCircleIcon -} from '@patternfly/react-icons'; - -type Props = { - cluster?: string; -}; - -export const IstioStatusInline: React.FC = (props: Props) => { - return ( - - ); -}; diff --git a/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx b/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx index 13966b2c55..af7544e068 100644 --- a/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx +++ b/frontend/src/components/IstioStatus/__tests__/IstioStatus.test.tsx @@ -3,7 +3,6 @@ import { ShallowWrapper, shallow } from 'enzyme'; import { ComponentStatus, Status } from '../../../types/IstioStatus'; import { IstioStatusComponent } from '../IstioStatus'; import { shallowToJson } from 'enzyme-to-json'; -import { i18n } from 'i18n'; const mockIcon = (componentList: ComponentStatus[]): ShallowWrapper => { return shallow( @@ -13,9 +12,6 @@ const mockIcon = (componentList: ComponentStatus[]): ShallowWrapper => { namespaces={[{ name: 'bookinfo' }, { name: 'istio-system' }]} setIstioStatus={jest.fn()} refreshNamespaces={jest.fn()} - t={(key: string) => key} - tReady={true} - i18n={i18n} /> ); }; @@ -27,7 +23,7 @@ const testSnapshot = (wrapper: any): void => { const testTooltip = (wrapper: any): void => { expect(wrapper.name()).toEqual('Tooltip'); - expect(wrapper.props().position).toEqual('left'); + expect(wrapper.props().position).toEqual('top'); expect(wrapper.props().enableFlip).toEqual(true); expect(wrapper.children().length).toEqual(1); }; diff --git a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap index 5165e01c3f..665fee3943 100644 --- a/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap +++ b/frontend/src/components/IstioStatus/__tests__/__snapshots__/IstioStatus.test.tsx.snap @@ -36,7 +36,7 @@ exports[`When addon component has a problem the Icon shows is displayed in orang } enableFlip={true} maxWidth="25rem" - position="left" + position="top" > { - + diff --git a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx index 5fa29fb3dc..d4569106e6 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelControlPlane.tsx @@ -35,6 +35,7 @@ import { ControlPlaneMetricsMap, Metric } from 'types/Metrics'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { MeshMTLSStatus } from 'components/MTls/MeshMTLSStatus'; +import { t } from 'utils/I18nUtils'; type TargetPanelControlPlaneProps = TargetPanelCommonProps & { meshStatus: string; @@ -154,8 +155,7 @@ export class TargetPanelControlPlane extends React.Component<
{data.version && (
- {`Version: `} - {data.version} + {`${t('Version')}: ${data.version}`}
)} @@ -193,6 +193,7 @@ export class TargetPanelControlPlane extends React.Component< )} {targetPanelHR} + {`${t('Configuration')}:`}
{JSON.stringify(data.infraData, null, 2)}
@@ -205,7 +206,7 @@ export class TargetPanelControlPlane extends React.Component<
<span className={nodeStyle}> - <span>Loading...</span> + <span>{t('Loading...')}</span> </span>
diff --git a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx index 30682d4bbd..1a595be23e 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelDataPlane.tsx @@ -11,120 +11,39 @@ import { ExpandableRowContent, Table, Tbody, Td, Th, Thead, Tr } from '@patternf import { NamespaceInfo } from 'types/NamespaceInfo'; import { TargetPanelDataPlaneNamespace } from './TargetPanelDataPlaneNamespace'; import { serverConfig } from 'config'; - -type TargetPanelDataPlaneState = { - expanded: string[]; - loading: boolean; - node?: Node; -}; - -const defaultState: TargetPanelDataPlaneState = { - expanded: [], - loading: false, - node: undefined -}; +import { t } from 'utils/I18nUtils'; +import { ControlPlaneVersionBadge } from 'pages/Overview/ControlPlaneVersionBadge'; const nodeStyle = kialiStyle({ alignItems: 'center', display: 'flex' }); -export class TargetPanelDataPlane extends React.Component { - constructor(props: TargetPanelCommonProps) { - super(props); - - const dataPlaneNode = this.props.target.elem as Node; - this.state = { ...defaultState, node: dataPlaneNode }; - } +export const TargetPanelDataPlane: React.FC = (props: TargetPanelCommonProps) => { + const [expanded, setExpanded] = React.useState([]); - render(): React.ReactNode { - if (!this.state.node) { - return null; - } - - const node = this.props.target.elem as Node; - const data = node.getData() as MeshNodeData; - - return ( -
-
{this.renderNodeHeader(data)}
-
-
- - - - - - {(data.infraData as NamespaceInfo[]) - .filter(ns => ns.name !== serverConfig.istioNamespace) - .sort((ns1, ns2) => (ns1.name < ns2.name ? -1 : 1)) - .map((ns, i) => { - return ( - - - - - - - - - ); - })} -
- Namespace
this.toggleExpanded(ns), - expandId: `ns-${ns.name}` - }} - /> - {ns.name}
- - - {targetPanelHR} -
-                            {JSON.stringify(
-                              data.infraData.find(id => id.name === ns.name),
-                              null,
-                              2
-                            )}
-                          
-
-
-
-
- ); - } - - private isExpanded = (ns: NamespaceInfo): boolean => { - return this.state.expanded.includes(ns.name); + const isExpanded = (ns: NamespaceInfo): boolean => { + return expanded.includes(ns.name); }; - private toggleExpanded = (ns: NamespaceInfo): void => { - const updatedExpanded = this.state.expanded.filter(n => ns.name !== n); - if (updatedExpanded.length === this.state.expanded.length) { + const toggleExpanded = (ns: NamespaceInfo): void => { + const updatedExpanded = expanded.filter(n => ns.name !== n); + if (updatedExpanded.length === expanded.length) { updatedExpanded.push(ns.name); } - this.setState({ expanded: updatedExpanded }); + setExpanded(updatedExpanded); }; - private renderNodeHeader = (data: MeshNodeData): React.ReactNode => { + const renderNodeHeader = (data: MeshNodeData): React.ReactNode => { return ( <span className={nodeStyle}> - <PFBadge badge={PFBadges.Namespace} size="sm" /> + <PFBadge badge={PFBadges.DataPlane} size="sm" /> {data.infraName} + {data.version && ( + <ControlPlaneVersionBadge isCanary={data.isCanary!} version={data.version}></ControlPlaneVersionBadge> + )} </span> @@ -134,4 +53,73 @@ export class TargetPanelDataPlane extends React.Component ); }; -} + + const node = props.target?.elem as Node; + + if (!node) { + return null; + } + + const data = node.getData() as MeshNodeData; + + return ( +
+
{renderNodeHeader(data)}
+
+ + + + + + + {(data.infraData as NamespaceInfo[]) + .filter(ns => ns.name !== serverConfig.istioNamespace) + .sort((ns1, ns2) => (ns1.name < ns2.name ? -1 : 1)) + .map((ns, i) => { + return ( + + + + + + + + + ); + })} +
+ Namespace
toggleExpanded(ns), + expandId: `ns-${ns.name}` + }} + /> + {ns.name}
+ + + {targetPanelHR} + {`${t('Configuration')}:`} +
+                          {JSON.stringify(
+                            data.infraData.find((id: NamespaceInfo) => id.name === ns.name),
+                            null,
+                            2
+                          )}
+                        
+
+
+
+
+ ); +}; diff --git a/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx b/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx index a526ee4fea..1a70273439 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelDataPlaneNamespace.tsx @@ -31,6 +31,7 @@ import { TLSStatus } from 'types/TLSStatus'; import * as FilterHelper from '../../../components/FilterList/FilterHelper'; import { panelBodyStyle, panelHeadingStyle } from 'pages/Graph/SummaryPanelStyle'; import { Metric } from 'types/Metrics'; +import { t } from 'utils/I18nUtils'; type TargetPanelDataPlaneNamespaceProps = Omit & { isExpanded: boolean; @@ -98,6 +99,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< componentDidUpdate(prevProps: TargetPanelDataPlaneNamespaceProps): void { const shouldLoad = prevProps.updateTime !== this.props.updateTime || (!prevProps.isExpanded && this.props.isExpanded); + if (shouldLoad) { this.load(); } @@ -169,7 +171,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< <span className={namespaceNameStyle}> - <span>Loading...</span> + <span>{t('Loading...')}</span> </span> @@ -251,7 +253,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< return namespaceActions; }; - private renderNamespaceBadges(ns: NamespaceInfo, tooltip: boolean): React.ReactNode { + private renderNamespaceBadges = (ns: NamespaceInfo, tooltip: boolean): React.ReactNode => { return ( <> {serverConfig.ambientEnabled && ns.labels && ns.isAmbient && ( @@ -259,9 +261,9 @@ export class TargetPanelDataPlaneNamespace extends React.Component< )} ); - } + }; - private renderLabels(ns: NamespaceInfo): React.ReactNode { + private renderLabels = (ns: NamespaceInfo): React.ReactNode => { const labelsLength = ns.labels ? `${Object.entries(ns.labels).length}` : 'No'; const labelContent = ns.labels ? ( @@ -291,9 +293,9 @@ export class TargetPanelDataPlaneNamespace extends React.Component< ); return labelContent; - } + }; - private renderIstioConfigStatus(ns: NamespaceInfo): React.ReactNode { + private renderIstioConfigStatus = (ns: NamespaceInfo): React.ReactNode => { let validations: ValidationStatus = { errors: 0, namespace: ns.name, objectCount: 0, warnings: 0 }; if (!!ns.validations) { @@ -316,7 +318,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< /> ); - } + }; private load = (): void => { this.promises.cancelAll(); @@ -364,7 +366,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component< this.setState({ loading: true }); }; - private fetchHealthStatus(): Promise { + private fetchHealthStatus = async (): Promise => { const cluster = this.props.targetCluster; const namespace = this.props.targetNamespace; return API.getClustersAppHealth(namespace, this.props.duration, cluster!) @@ -398,9 +400,9 @@ export class TargetPanelDataPlaneNamespace extends React.Component< this.setState({ status: nsStatus }); }) .catch(err => this.handleApiError('Could not fetch namespace health', err)); - } + }; - private fetchMetrics(direction: DirectionType): Promise { + private fetchMetrics = async (direction: DirectionType): Promise => { const rateParams = computePrometheusRateParams(this.props.duration, 10); const options: IstioMetricsOptions = { filters: ['request_count', 'request_error_count'], @@ -425,13 +427,13 @@ export class TargetPanelDataPlaneNamespace extends React.Component< ); }) .catch(err => this.handleApiError(`Could not fetch ${direction} metrics for namespace [${namespace}]`, err)); - } + }; - private handleApiError(message: string, error: ApiError): void { + private handleApiError = (message: string, error: ApiError): void => { FilterHelper.handleError(`${message}: ${API.getErrorString(error)}`); - } + }; - private renderCharts(direction: DirectionType): React.ReactNode { + private renderCharts = (direction: DirectionType): React.ReactNode => { if (this.state.status) { const namespace = this.props.targetNamespace; @@ -450,9 +452,9 @@ export class TargetPanelDataPlaneNamespace extends React.Component< } return
Namespace metrics are not available
; - } + }; - private renderStatus(): React.ReactNode { + private renderStatus = (): React.ReactNode => { const targetPage = switchType(healthType, Paths.APPLICATIONS, Paths.SERVICES, Paths.WORKLOADS); const namespace = this.props.targetNamespace; const status = this.state.status; @@ -545,7 +547,7 @@ export class TargetPanelDataPlaneNamespace extends React.Component<
); - } + }; private show = (showType: Show, namespace: string, graphType: string): void => { let destination = ''; diff --git a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx index d73756ae5b..ceff2a5a08 100644 --- a/frontend/src/pages/Mesh/target/TargetPanelNode.tsx +++ b/frontend/src/pages/Mesh/target/TargetPanelNode.tsx @@ -7,6 +7,7 @@ import { MeshInfraType, MeshNodeData, isExternal } from 'types/Mesh'; import { classes } from 'typestyle'; import { panelBodyStyle, panelHeadingStyle, panelStyle } from 'pages/Graph/SummaryPanelStyle'; import { Title, TitleSizes } from '@patternfly/react-core'; +import { t } from 'utils/I18nUtils'; type TargetPanelNodeProps = TargetPanelCommonProps; @@ -111,13 +112,8 @@ export class TargetPanelNode extends React.Component
{renderNodeHeader(data, isExternal(data.cluster))}
- {data.version && ( -
- {`Version: `} - {data.version} -
-
- )} + {data.version &&
{`${t('Version')}: ${data.version}`}
} + {`${t('Configuration')}:`}
{JSON.stringify(data.infraData, null, 2)}
diff --git a/frontend/src/pages/Overview/ControlPlaneBadge.tsx b/frontend/src/pages/Overview/ControlPlaneBadge.tsx index 00273ccd7a..fbb8fd22d8 100644 --- a/frontend/src/pages/Overview/ControlPlaneBadge.tsx +++ b/frontend/src/pages/Overview/ControlPlaneBadge.tsx @@ -1,13 +1,19 @@ import { Label, Tooltip } from '@patternfly/react-core'; import * as React from 'react'; -import { IstioStatusInline } from '../../components/IstioStatus/IstioStatusInline'; -import { config, serverConfig } from '../../config'; +import { serverConfig } from '../../config'; import { AmbientBadge } from '../../components/Ambient/AmbientBadge'; import { RemoteClusterBadge } from './RemoteClusterBadge'; import { isRemoteCluster } from './OverviewCardControlPlaneNamespace'; import { useTranslation } from 'react-i18next'; import { I18N_NAMESPACE } from 'types/Common'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; +import { IstioStatus, meshLinkStyle } from 'components/IstioStatus/IstioStatus'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + ExclamationTriangleIcon, + MinusCircleIcon +} from '@patternfly/react-icons'; type Props = { annotations?: { [key: string]: string }; @@ -16,15 +22,23 @@ type Props = { export const ControlPlaneBadge: React.FC = (props: Props) => { const { t } = useTranslation(I18N_NAMESPACE); + const { pathname } = useLocation(); + // Remote clusters do not have istio status because istio is not running there // so don't display istio status badge for those. return ( <> - {config.about.mesh.linkText} - + <> + {t('It belongs to the istio control plane')} + {!pathname.endsWith('/mesh') && ( +
+ {t('More info at')} + {t('Mesh page')} +
+ )} + } >