diff --git a/kmesh/src/components/nodeinfo/Detail.tsx b/kmesh/src/components/nodeinfo/Detail.tsx
index 77537c7a2e..ade72953eb 100644
--- a/kmesh/src/components/nodeinfo/Detail.tsx
+++ b/kmesh/src/components/nodeinfo/Detail.tsx
@@ -2,11 +2,14 @@
* Detail view for a single KmeshNodeInfo resource.
* Shows full IPsec security state for one node: SPI, addresses, Pod CIDRs, and boot ID.
*/
+import { K8s } from '@kinvolk/headlamp-plugin/lib';
import {
MainInfoSection,
+ SectionBox,
SimpleTable,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
+import { Link as HeadlampLink } from '@kinvolk/headlamp-plugin/lib/components/common';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
@@ -15,8 +18,54 @@ import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import React from 'react';
import { useParams } from 'react-router-dom';
+import { useKmeshDaemonPods } from '../../hooks/useKmeshDaemonPods';
import { KmeshNodeInfo } from '../../resources/kmeshNodeInfo';
+/** Related resources for a KmeshNodeInfo: the underlying Node and the kmesh-daemon Pod on it. */
+function NodeInfoRelatedResources({ nodeName }: { nodeName: string }) {
+ const [node] = K8s.ResourceClasses.Node.useGet(nodeName);
+ const { pods: daemonPods } = useKmeshDaemonPods();
+ const daemonPod = daemonPods.find(p => p.nodeName === nodeName) ?? null;
+ const [daemonPodObject] = K8s.ResourceClasses.Pod.useGet(
+ daemonPod?.name ?? '',
+ daemonPod?.namespace ?? ''
+ );
+
+ return (
+
+ row.label,
+ },
+ {
+ label: 'Resource',
+ getter: (row: { label: string; resources: any[] }) =>
+ row.resources.length > 0 ? (
+
+ {row.resources.map(resource => (
+
+ {resource.getName()}
+
+ ))}
+
+ ) : (
+
+ Not found
+
+ ),
+ },
+ ]}
+ />
+
+ );
+}
+
/**
* Detail view for a single KmeshNodeInfo resource.
*/
@@ -169,6 +218,10 @@ export default function KmeshNodeInfoDetail() {
]}
/>
+
+
+
+
);
}
diff --git a/kmesh/src/components/waypoints/Detail.test.tsx b/kmesh/src/components/waypoints/Detail.test.tsx
new file mode 100644
index 0000000000..0a22dc072a
--- /dev/null
+++ b/kmesh/src/components/waypoints/Detail.test.tsx
@@ -0,0 +1,129 @@
+import { cleanup, render, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const { mockUseGet, mockPodUseList, mockServiceUseList, mockNamespaceUseList, mockUseParams } =
+ vi.hoisted(() => ({
+ mockUseGet: vi.fn(),
+ mockPodUseList: vi.fn(),
+ mockServiceUseList: vi.fn(),
+ mockNamespaceUseList: vi.fn(),
+ mockUseParams: vi.fn(() => ({})),
+ }));
+
+vi.mock('react-router-dom', () => ({
+ useParams: mockUseParams,
+}));
+
+vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
+ K8s: {
+ ResourceClasses: {
+ Pod: { useList: mockPodUseList },
+ Service: { useList: mockServiceUseList },
+ Namespace: { useList: mockNamespaceUseList },
+ },
+ },
+}));
+
+vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
+ ObjectEventList: () => null,
+ SectionBox: ({ title, children }: any) =>
{children}
,
+ SimpleTable: ({ data, columns }: any) => (
+
+
+ {data.map((row: any, i: number) => (
+
+ {columns.map((col: any) => (
+ | {col.getter(row)} |
+ ))}
+
+ ))}
+
+
+ ),
+ StatusLabel: ({ children }: any) => {children},
+}));
+
+vi.mock('@kinvolk/headlamp-plugin/lib/components/common', () => ({
+ MainInfoSection: ({ extraInfo }: any) => (
+
+ {extraInfo?.map((info: any) => (
+
+ {info.name}: {String(info.value)}
+
+ ))}
+
+ ),
+ Link: ({ kubeObject, children }: any) => {children ?? kubeObject?.getName()},
+}));
+
+vi.mock('../../resources/waypoint', () => ({
+ Waypoint: { useGet: mockUseGet },
+}));
+
+import WaypointDetail from './Detail';
+
+beforeEach(() => {
+ mockPodUseList.mockReturnValue([[]]);
+ mockServiceUseList.mockReturnValue([[]]);
+ mockNamespaceUseList.mockReturnValue([[]]);
+});
+
+afterEach(() => {
+ cleanup();
+ mockUseGet.mockReset();
+ mockPodUseList.mockReset();
+ mockServiceUseList.mockReset();
+ mockNamespaceUseList.mockReset();
+ mockUseParams.mockReset().mockReturnValue({});
+});
+
+function waypoint(overrides: Record = {}) {
+ return {
+ spec: { gatewayClassName: 'kmesh-waypoint' },
+ status: { conditions: [] },
+ image: 'kmesh/waypoint:latest',
+ currentStatus: 'Programmed',
+ getName: () => 'my-waypoint',
+ metadata: { uid: 'waypoint-uid', namespace: 'default' },
+ ...overrides,
+ };
+}
+
+function pod(name: string) {
+ return { getName: () => name, metadata: { uid: `pod-${name}` } };
+}
+
+function service(name: string) {
+ return { getName: () => name, metadata: { uid: `svc-${name}` } };
+}
+
+function namespace(name: string, labels: Record = {}) {
+ return { getName: () => name, metadata: { uid: `ns-${name}`, labels } };
+}
+
+describe('WaypointDetail related resources', () => {
+ it('shows "None found" for all related resource rows when nothing matches', () => {
+ mockUseParams.mockReturnValue({ namespace: 'default', name: 'my-waypoint' });
+ mockUseGet.mockReturnValue([waypoint(), null]);
+
+ render();
+
+ expect(screen.getAllByText('None found')).toHaveLength(3);
+ });
+
+ it('renders links for proxy pods, the proxy service, and enrolled namespaces', () => {
+ mockUseParams.mockReturnValue({ namespace: 'default', name: 'my-waypoint' });
+ mockUseGet.mockReturnValue([waypoint(), null]);
+ mockPodUseList.mockReturnValue([[pod('my-waypoint-abcde')]]);
+ mockServiceUseList.mockReturnValue([[service('my-waypoint')]]);
+ mockNamespaceUseList.mockReturnValue([
+ [namespace('team-a', { 'istio.io/use-waypoint': 'my-waypoint' }), namespace('team-b')],
+ ]);
+
+ render();
+
+ expect(screen.getByText('my-waypoint-abcde')).toBeTruthy();
+ expect(screen.getByText('team-a')).toBeTruthy();
+ expect(screen.queryByText('team-b')).toBeNull();
+ });
+});
diff --git a/kmesh/src/components/waypoints/Detail.tsx b/kmesh/src/components/waypoints/Detail.tsx
index 2d0e8d98f3..7c8b7fae18 100644
--- a/kmesh/src/components/waypoints/Detail.tsx
+++ b/kmesh/src/components/waypoints/Detail.tsx
@@ -1,14 +1,25 @@
+import { K8s } from '@kinvolk/headlamp-plugin/lib';
import {
ObjectEventList,
SectionBox,
SimpleTable,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
-import { MainInfoSection } from '@kinvolk/headlamp-plugin/lib/components/common';
+import {
+ Link as HeadlampLink,
+ MainInfoSection,
+} from '@kinvolk/headlamp-plugin/lib/components/common';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
import { useParams } from 'react-router-dom';
import { Waypoint } from '../../resources/waypoint';
import { kmeshRoutePaths } from '../../utils/kmeshRoutes';
+/** Label the Gateway API deployer sets on the proxy Pod/Service it creates for a Gateway. */
+const GATEWAY_NAME_LABEL = 'istio.io/gateway-name';
+/** Label a Namespace carries when its workloads are enrolled to use a specific waypoint. */
+const USE_WAYPOINT_LABEL = 'istio.io/use-waypoint';
+
/**
* Props for the Waypoint Detail view component.
*
@@ -92,6 +103,70 @@ function ConditionsTable({ conditions }: { conditions?: any[] }) {
);
}
+interface RelatedResourceRow {
+ label: string;
+ resources: any[];
+}
+
+function RelatedResourcesTable({ rows }: { rows: RelatedResourceRow[] }) {
+ return (
+
+ row.label,
+ },
+ {
+ label: 'Resources',
+ getter: (row: RelatedResourceRow) =>
+ row.resources.length > 0 ? (
+
+ {row.resources.map(resource => (
+
+ {resource.getName()}
+
+ ))}
+
+ ) : (
+
+ None found
+
+ ),
+ },
+ ]}
+ />
+
+ );
+}
+
+function WaypointRelatedResources({ name, namespace }: { name: string; namespace: string }) {
+ const [proxyPods] = K8s.ResourceClasses.Pod.useList({
+ namespace,
+ labelSelector: `${GATEWAY_NAME_LABEL}=${name}`,
+ });
+ const [proxyServices] = K8s.ResourceClasses.Service.useList({
+ namespace,
+ labelSelector: `${GATEWAY_NAME_LABEL}=${name}`,
+ });
+ const [namespaces] = K8s.ResourceClasses.Namespace.useList();
+
+ const enrolledNamespaces = (namespaces ?? []).filter(
+ ns => ns.metadata?.labels?.[USE_WAYPOINT_LABEL] === name
+ );
+
+ return (
+
+ );
+}
+
function WaypointDetailContent({
name,
namespace,
@@ -126,6 +201,7 @@ function WaypointDetailContent({
]}
/>
{waypoint && }
+ {waypoint && }
{waypoint && }
>
);