Skip to content

feat(topology): add member cluster workload detail drawer to topology graph#531

Open
SunsetB612 wants to merge 1 commit into
karmada-io:mainfrom
SunsetB612:feat/topology-member-workload-detail
Open

feat(topology): add member cluster workload detail drawer to topology graph#531
SunsetB612 wants to merge 1 commit into
karmada-io:mainfrom
SunsetB612:feat/topology-member-workload-detail

Conversation

@SunsetB612
Copy link
Copy Markdown
Contributor

No description provided.

@karmada-bot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign kevin-wangzefeng for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@karmada-bot karmada-bot requested review from jhnine and warjiang April 29, 2026 14:22
@karmada-bot karmada-bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Apr 29, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a resource topology visualization feature to the Karmada dashboard, allowing users to trace propagation chains from control-plane workloads to member cluster resources and Pods. It includes a new backend API with custom informer indexers and a frontend graph view using React Flow. Feedback focuses on improving context propagation for request cancellation, optimizing Pod lookups with label selectors, extending health status support to more workload types, and enhancing UI robustness through better error handling and the restoration of terminal resize functionality.

Comment thread cmd/api/app/routes/topology/handler.go Outdated
return
}

result, err := topology.GetResourceTopology(k8sClient, namespace, name, kind)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The request context should be passed to GetResourceTopology to ensure that any downstream operations (like API calls to member clusters) can be cancelled if the client disconnects.

Suggested change
result, err := topology.GetResourceTopology(k8sClient, namespace, name, kind)
result, err := topology.GetResourceTopology(c.Request.Context(), k8sClient, namespace, name, kind)

Comment thread pkg/resource/topology/topology.go Outdated
Comment on lines +26 to +29
func GetResourceTopology(
k8sClient kubeclient.Interface,
namespace, name, kind string) (*TopologyResponse, error) {
return traceChain(context.TODO(), k8sClient, namespace, name, kind)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using context.TODO() in a function that is part of a request handling chain. The function should accept a context.Context parameter and pass it down to traceChain.

Suggested change
func GetResourceTopology(
k8sClient kubeclient.Interface,
namespace, name, kind string) (*TopologyResponse, error) {
return traceChain(context.TODO(), k8sClient, namespace, name, kind)
func GetResourceTopology(
ctx context.Context,
k8sClient kubeclient.Interface,
namespace, name, kind string) (*TopologyResponse, error) {
return traceChain(ctx, k8sClient, namespace, name, kind)
}

Comment thread pkg/resource/topology/chain.go Outdated
return nil, err
}
ownerUIDs := getPodDirectOwnerUIDs(ctx, memberClient, namespace, kind, workloadUID)
podList, err := memberClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Listing all pods in a namespace and filtering them in-memory is inefficient, especially in large clusters with many resources. It is better to use a label selector in the ListOptions to fetch only the relevant pods. Since the workload object is already fetched in previous steps, its selector should be used here.

Comment on lines +321 to +335
switch kind {
case "Deployment":
deploy, err := memberClient.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
klog.V(4).InfoS("Failed to get member deployment", "cluster", clusterName, "err", err)
return NodeStatusAbnormal
}
if deploy.Spec.Replicas != nil && deploy.Status.ReadyReplicas == *deploy.Spec.Replicas {
return NodeStatusHealthy
}
return NodeStatusProgressing
default:
return NodeStatusHealthy
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getMemberWorkloadStatus function currently only handles Deployment. It should be extended to support other workload types like StatefulSet and DaemonSet to provide accurate health status in the topology graph.

func getMemberWorkloadStatus(ctx context.Context, clusterName, namespace, name, kind string) NodeStatus {
	memberClient := client.InClusterClientForMemberCluster(clusterName)
	if memberClient == nil {
		return NodeStatusAbnormal
	}
	switch kind {
	case "Deployment":
		deploy, err := memberClient.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
		if err != nil {
			klog.V(4).InfoS("Failed to get member deployment", "cluster", clusterName, "err", err)
			return NodeStatusAbnormal
		}
		if deploy.Spec.Replicas != nil && deploy.Status.ReadyReplicas == *deploy.Spec.Replicas {
			return NodeStatusHealthy
		}
		return NodeStatusProgressing
	case "StatefulSet":
		sts, err := memberClient.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{})
		if err != nil {
			klog.V(4).InfoS("Failed to get member statefulset", "cluster", clusterName, "err", err)
			return NodeStatusAbnormal
		}
		if sts.Spec.Replicas != nil && sts.Status.ReadyReplicas == *sts.Spec.Replicas {
			return NodeStatusHealthy
		}
		return NodeStatusProgressing
	case "DaemonSet":
		ds, err := memberClient.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{})
		if err != nil {
			klog.V(4).InfoS("Failed to get member daemonset", "cluster", clusterName, "err", err)
			return NodeStatusAbnormal
		}
		if ds.Status.NumberReady == ds.Status.DesiredNumberScheduled {
			return NodeStatusHealthy
		}
		return NodeStatusProgressing
	default:
		return NodeStatusHealthy
	}
}

Comment on lines +76 to +94
void Promise.all([
GetMemberClusterWorkloadDetail({ memberClusterName, namespace, name, kind }),
GetMemberClusterWorkloadEvents({ memberClusterName, namespace, name, kind }),
GetMemberClusterPods({ memberClusterName, namespace }),
]).then(([detailResp, eventsRet, podsRet]) => {
const workloadDetail = (detailResp?.data ?? {}) as WorkloadDetail;
setViewDetail(workloadDetail);
setViewEvents(eventsRet?.data?.events || []);
const selector = workloadDetail.selector;
setViewPods(
selector
? (podsRet?.data?.pods || []).filter((pod) =>
Object.entries(selector).every(
([k, v]) => pod.objectMeta.labels?.[k] === v,
),
)
: [],
);
}).finally(() => setViewLoading(false));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Promise.all without individual error handling means that if any of the requests (like fetching events) fails, the entire data loading process will fail, and the user won't see the workload details. Consider using Promise.allSettled or handling errors for each request individually.

    Promise.allSettled([
      GetMemberClusterWorkloadDetail({ memberClusterName, namespace, name, kind }),
      GetMemberClusterWorkloadEvents({ memberClusterName, namespace, name, kind }),
      GetMemberClusterPods({ memberClusterName, namespace }),
    ]).then(([detailRes, eventsRes, podsRes]) => {
      if (detailRes.status === 'fulfilled') {
        const workloadDetail = (detailRes.value?.data ?? {}) as WorkloadDetail;
        setViewDetail(workloadDetail);
        const selector = workloadDetail.selector;
        if (podsRes.status === 'fulfilled') {
          setViewPods(
            selector
              ? (podsRes.value?.data?.pods || []).filter((pod) =>
                  Object.entries(selector).every(([k, v]) => pod.objectMeta.labels?.[k] === v)
                )
              : []
          );
        }
      }
      if (eventsRes.status === 'fulfilled') {
        setViewEvents(eventsRes.value?.data?.events || []);
      }
    }).finally(() => setViewLoading(false));

else if (frame.Op === 'toast' && frame.Data) void messageApi.info(frame.Data);
} catch { /* ignore */ }
};
term.onData((data) => { sock.send(JSON.stringify({ Op: 'stdin', Data: data })); });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The terminal resize listener was removed during refactoring. This is a regression that will cause the terminal to not adjust its dimensions when the drawer or window is resized.

Suggested change
term.onData((data) => { sock.send(JSON.stringify({ Op: 'stdin', Data: data })); });
term.onData((data) => { sock.send(JSON.stringify({ Op: 'stdin', Data: data })); });
term.onResize(({ cols, rows }) => { sock.send(JSON.stringify({ Op: 'resize', Cols: cols, Rows: rows })); });

Comment on lines +69 to +81
<Select
placeholder="Resource Name"
className="min-w-[220px]"
showSearch
allowClear
value={name || undefined}
onChange={(v) => setName(v || '')}
options={[]}
mode={undefined}
searchValue={name}
onSearch={(v) => setName(v)}
notFoundContent={null}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a Select component with empty options for the resource name is confusing for users. Since the user is expected to type the name, an Input component or an AutoComplete component would provide a better user experience.

Suggested change
<Select
placeholder="Resource Name"
className="min-w-[220px]"
showSearch
allowClear
value={name || undefined}
onChange={(v) => setName(v || '')}
options={[]}
mode={undefined}
searchValue={name}
onSearch={(v) => setName(v)}
notFoundContent={null}
/>
<Input
placeholder="Resource Name"
className="min-w-[220px]"
allowClear
value={name}
onChange={(e) => setName(e.target.value)}
/>

@SunsetB612 SunsetB612 force-pushed the feat/topology-member-workload-detail branch 3 times, most recently from d96f2d1 to fec8cce Compare May 12, 2026 08:12
@SunsetB612 SunsetB612 force-pushed the feat/topology-member-workload-detail branch from fec8cce to 40fa6ae Compare May 13, 2026 04:02
@SunsetB612 SunsetB612 changed the title feat(topology): reuse member cluster workload detail drawer on topology member workload node click feat(topology): add member cluster workload detail drawer to topology graph May 13, 2026
@SunsetB612 SunsetB612 force-pushed the feat/topology-member-workload-detail branch 2 times, most recently from 9f8bf0c to 54741c4 Compare May 13, 2026 07:25
@SunsetB612 SunsetB612 force-pushed the feat/topology-member-workload-detail branch 2 times, most recently from 016e541 to e6b7517 Compare May 25, 2026 03:37
@warjiang
Copy link
Copy Markdown
Contributor

@SunsetB612 sorry for that there is still some conflict ~ I want to review this PR, feel free to resovle the conflict

@SunsetB612
Copy link
Copy Markdown
Contributor Author

@SunsetB612 sorry for that there is still some conflict ~ I want to review this PR, feel free to resovle the conflict

Sorry about that, I’ll resolve the conflicts tonight. Thanks for reviewing the PR~

… graph

Signed-off-by: SunsetB612 <10235101575@stu.ecnu.edu.cn>
@SunsetB612 SunsetB612 force-pushed the feat/topology-member-workload-detail branch from e6b7517 to d456a22 Compare May 27, 2026 14:40
@karmada-bot karmada-bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels May 27, 2026
@SunsetB612
Copy link
Copy Markdown
Contributor Author

SunsetB612 commented May 27, 2026

@SunsetB612 sorry for that there is still some conflict ~ I want to review this PR, feel free to resovle the conflict

Sorry about that, I’ll resolve the conflicts tonight. Thanks for reviewing the PR~

@warjiang the conflicts have been resolved now. thanks again for reviewing the PR ❤️ ~

@warjiang
Copy link
Copy Markdown
Contributor

/assign

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants