This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch refactor/decompose-large-files
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 273433cb5b7325ceb180442e21ed6cf68744e922
Author: Wu Sheng <[email protected]>
AuthorDate: Sat Jun 27 00:28:18 2026 +0800

    feat(profiling): validate network task instance has processes before Create
    
    The network create modal lists the selected instance's rover-monitored 
processes (new BFF listProcesses route -> api-client networkProfile.processes) 
and disables Create with a clear reason when the instance has none. OAP rejects 
such a task, so this surfaces it upfront instead of failing on submit. Fetch 
extracted to useNetworkProcesses.
---
 apps/bff/src/http/query/ebpf.ts                    | 43 +++++++++++++++++++
 apps/ui/src/api/client.ts                          |  2 +
 apps/ui/src/api/scopes/network-profile.ts          | 11 +++++
 .../layer/profiling/LayerNetworkProfilingView.vue  | 49 ++++++++++++----------
 apps/ui/src/layer/profiling/useNetworkProcesses.ts | 47 +++++++++++++++++++++
 packages/api-client/src/ebpf.ts                    | 13 ++++++
 packages/api-client/src/index.ts                   |  2 +
 7 files changed, 144 insertions(+), 23 deletions(-)

diff --git a/apps/bff/src/http/query/ebpf.ts b/apps/bff/src/http/query/ebpf.ts
index 5202842..e9a1e40 100644
--- a/apps/bff/src/http/query/ebpf.ts
+++ b/apps/bff/src/http/query/ebpf.ts
@@ -39,6 +39,7 @@ import type {
   EBPFTaskCreationResponse,
   EBPFTaskListResponse,
   FetchLike,
+  NetworkProcessesResponse,
   NetworkProfilingCreateRequest,
   NetworkProfilingCreateResponse,
   NetworkProfilingKeepAliveResponse,
@@ -359,6 +360,15 @@ function relationSeries(env: MqeEnv | undefined): 
Array<number | null> {
   });
 }
 
+const LIST_PROCESSES = /* GraphQL */ `
+  query listNetworkProcesses($instanceId: ID!, $duration: Duration!) {
+    listProcesses(instanceId: $instanceId, duration: $duration) {
+      id
+      name
+    }
+  }
+`;
+
 export function registerEBPFRoutes(app: FastifyInstance, deps: EBPFRouteDeps): 
void {
   const auth = requireAuth(deps);
 
@@ -574,6 +584,39 @@ export function registerEBPFRoutes(app: FastifyInstance, 
deps: EBPFRouteDeps): v
     },
   );
 
+  /** Processes reporting on a service instance — the network task-creation
+   *  modal confirms the instance has profilable processes before Create
+   *  (OAP rejects a network task on an instance with none). */
+  app.get(
+    '/api/ebpf/network/processes',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const q = req.query as { serviceInstance?: string; windowMinutes?: 
string };
+      const instance = (q.serviceInstance ?? '').trim();
+      const payload: NetworkProcessesResponse = { processes: [], reachable: 
true };
+      if (!instance) return reply.send(payload);
+      const minutes = Math.max(5, Math.min(180, Number(q.windowMinutes) || 
30));
+      const endMs = Date.now();
+      const startMs = endMs - minutes * 60_000;
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+      const offset = await getServerOffsetMinutes(deps.config, deps.fetch);
+      try {
+        const data = await graphqlPost<{ listProcesses: 
NetworkProcessesResponse['processes'] }>(
+          opts,
+          LIST_PROCESSES,
+          {
+            instanceId: instance,
+            duration: { start: fmtMinute(startMs, offset), end: 
fmtMinute(endMs, offset), step: 'MINUTE' },
+          },
+        );
+        payload.processes = data.listProcesses ?? [];
+        return reply.send(payload);
+      } catch (err) {
+        return reply.send(softErr(payload, err));
+      }
+    },
+  );
+
   /** Create a network-profile task on a specific service instance. */
   app.post(
     '/api/ebpf/network/tasks',
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index aff6ab5..f17c912 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -221,6 +221,8 @@ export type {
   ProcessRelationEndpointRef,
   ProcessRelationMetric,
   ProcessRelationMetricsResponse,
+  NetworkProcess,
+  NetworkProcessesResponse,
   NetworkProfilingSampling,
   NetworkProfilingCreateRequest,
   NetworkProfilingCreateResponse,
diff --git a/apps/ui/src/api/scopes/network-profile.ts 
b/apps/ui/src/api/scopes/network-profile.ts
index 4db25e5..1944491 100644
--- a/apps/ui/src/api/scopes/network-profile.ts
+++ b/apps/ui/src/api/scopes/network-profile.ts
@@ -17,6 +17,7 @@
 
 import type {
   EBPFTaskListResponse,
+  NetworkProcessesResponse,
   NetworkProfilingCreateRequest,
   NetworkProfilingCreateResponse,
   NetworkProfilingKeepAliveResponse,
@@ -65,6 +66,16 @@ export class NetworkProfileApi {
     );
   }
 
+  /** Rover-monitored processes on an instance — the create modal confirms
+   *  the instance has profilable processes before Create. */
+  processes(serviceInstance: string, windowMinutes = 30): 
Promise<NetworkProcessesResponse> {
+    const qs = new URLSearchParams({ serviceInstance, windowMinutes: 
String(windowMinutes) });
+    return this.bff.request<NetworkProcessesResponse>(
+      'GET',
+      `/api/ebpf/network/processes?${qs.toString()}`,
+    );
+  }
+
   create(body: NetworkProfilingCreateRequest): 
Promise<NetworkProfilingCreateResponse> {
     return this.bff.request<NetworkProfilingCreateResponse>(
       'POST',
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index fdfef08..5904388 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -47,6 +47,7 @@ import type {
   ProcessRelationMetricsResponse,
 } from '@/api/client';
 import ProcessTopologyGraph from '@/layer/profiling/ProcessTopologyGraph.vue';
+import { useNetworkProcesses } from '@/layer/profiling/useNetworkProcesses';
 import TimeChart from '@/components/charts/TimeChart.vue';
 import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
 import Icon from '@/components/icons/Icon.vue';
@@ -58,9 +59,7 @@ const layerKey = computed(() => String(route.params.layerKey 
?? ''));
 const previewProcessTopology = usePreviewLayerBlock(layerKey, 
'processTopology');
 const { selectedId: serviceId } = useSelectedService();
 
-// Instance picker (binds to ?serviceInstance= via plain ref state — the
-// network view needs an *instance* to be useful, so we don't reuse the
-// generic `useSelectedInstance` composable's URL contract).
+// Instance picker — plain ref state (not the URL-bound useSelectedInstance).
 const instances = useLayerInstances(layerKey, serviceId);
 const selectedInstanceId = ref<string | null>(null);
 watch(
@@ -79,12 +78,8 @@ const tasksError = ref<string | null>(null);
 const tasksLoading = ref(false);
 const currentTask = ref<EBPFTask | null>(null);
 
-// Tasks are listed per SERVICE, not per selected instance. A network
-// task runs against the instance that was live when it was created; if
-// that pod has since been replaced (new pod name), AND-ing the task
-// query with the currently-selected instance hides the task entirely.
-// The task object carries its own serviceInstanceId, so the list stays
-// correct and selecting a task can still drive the right topology.
+// Tasks are listed per SERVICE, not the selected instance — a task's pod
+// may have been replaced; the task carries its own serviceInstanceId.
 watch(
   () => layerKey.value + '|' + (serviceId.value ?? ''),
   () => void refreshTasks(),
@@ -122,11 +117,8 @@ const topologyLoading = ref(false);
 const topologyError = ref<string | null>(null);
 const windowMinutes = ref(30);
 
-// Topology follows the SELECTED TASK: a finished FIXED_TIME task only
-// captured process relations on its own instance during its own window
-// (and that pod may since have been replaced). When a task is selected
-// we query its instance + [taskStartTime, taskStartTime+duration]; with
-// no task we fall back to the picker instance + rolling window.
+// Topology follows the selected task (its instance + capture window); with
+// no task, the picker instance + a rolling window.
 watch([currentTask, selectedInstanceId], () => void loadTopology());
 
 async function loadTopology(): Promise<void> {
@@ -187,9 +179,7 @@ function endpointRef(n: ProcessNode): 
ProcessRelationEndpointRef {
   };
 }
 
-// Refire when the operator clicks a different edge. The detail area
-// resets first (cascade-clear), then resolves async — never leaves a
-// stale conversation's numbers under the new edge's header.
+// Refire on a new edge: reset first (cascade-clear), then resolve async.
 watch(selectedCall, async (call) => {
   relationMetrics.value = null;
   relationError.value = null;
@@ -292,11 +282,11 @@ onBeforeUnmount(() => 
window.removeEventListener('keydown', onEdgeKeydown));
 const showNewTask = ref(false);
 useEscapeToClose(() => showNewTask.value, () => (showNewTask.value = false));
 const newTaskError = ref<string | null>(null);
+const { processes: networkProcesses, loading: processesLoading } =
+  useNetworkProcesses(selectedInstanceId, showNewTask);
 const { polling, pollRound, pollForNewTask } = useNewTaskPoll();
-// OAP's `EBPFNetworkDataCollectingSettings.requireCompleteRequest` and
-// `requireCompleteResponse` are `Boolean!` — non-null. Every sampling
-// row MUST carry the settings block, otherwise
-// `createEBPFNetworkProfiling` 400s with a schema validation error.
+// requireComplete{Request,Response} are Boolean! — every sampling row must
+// carry the settings block or create 400s.
 const DEFAULT_SETTINGS = (): NetworkProfilingSampling['settings'] => ({
   requireCompleteRequest: true,
   requireCompleteResponse: true,
@@ -497,6 +487,16 @@ function fmtTime(ms: number): string {
         <div v-if="!instances.instances.value.length" class="banner err">
           No instances available for this service — a network profile task 
cannot be created.
         </div>
+        <template v-else-if="selectedInstanceId">
+          <div v-if="processesLoading" class="hint">Checking processes on this 
instance…</div>
+          <div v-else-if="!networkProcesses.length" class="banner err">
+            This instance has no profilable processes — a network task cannot 
be created (network profiling needs a rover-monitored process).
+          </div>
+          <div v-else class="proc-list">
+            <span class="proc-list-label">Processes ({{ 
networkProcesses.length }})</span>
+            <span v-for="p in networkProcesses" :key="p.id" 
class="proc-chip">{{ p.name }}</span>
+          </div>
+        </template>
         <p class="hint">
           OAP captures one connection sample per matching rule. Leave URI
           empty to match any request; toggle 4xx/5xx to scope by status.
@@ -535,8 +535,8 @@ function fmtTime(ms: number): string {
         <button class="btn-secondary" @click="showNewTask = 
false">Cancel</button>
         <button
           class="btn-primary"
-          :disabled="!selectedInstanceId"
-          :title="selectedInstanceId ? 'Create the network profile task' : 'No 
instances available for this service'"
+          :disabled="!selectedInstanceId || processesLoading || 
!networkProcesses.length"
+          :title="!selectedInstanceId ? 'No instances available for this 
service' : processesLoading ? 'Checking processes…' : !networkProcesses.length 
? 'This instance has no profilable processes' : 'Create the network profile 
task'"
           @click="submitNewTask"
         >Create task</button>
       </div>
@@ -993,4 +993,7 @@ function fmtTime(ms: number): string {
   color: var(--sw-err);
   font-size: 11px;
 }
+.proc-list { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; 
margin: 6px 0; }
+.proc-list-label { font-size: 11px; color: var(--sw-fg-3); }
+.proc-chip { font-family: var(--sw-mono); font-size: 10.5px; color: 
var(--sw-fg-1); background: var(--sw-bg-2); border: 1px solid var(--sw-line); 
padding: 1px 6px; border-radius: 3px; }
 </style>
diff --git a/apps/ui/src/layer/profiling/useNetworkProcesses.ts 
b/apps/ui/src/layer/profiling/useNetworkProcesses.ts
new file mode 100644
index 0000000..94713a7
--- /dev/null
+++ b/apps/ui/src/layer/profiling/useNetworkProcesses.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ref, watch, type Ref } from 'vue';
+import { bffClient } from '@/api/client';
+import type { NetworkProcess } from '@/api/client';
+
+/**
+ * Processes reporting on a service instance. A network profiling task needs
+ * at least one (OAP rejects an instance with none), so the create modal
+ * fetches them lazily while `enabled` to validate + show them before Create.
+ */
+export function useNetworkProcesses(instanceId: Ref<string | null>, enabled: 
Ref<boolean>) {
+  const processes = ref<NetworkProcess[]>([]);
+  const loading = ref(false);
+  watch([enabled, instanceId], async ([on, id]) => {
+    if (!on || !id) {
+      processes.value = [];
+      return;
+    }
+    loading.value = true;
+    try {
+      const resp = await bffClient.networkProfile.processes(id);
+      if (!enabled.value || instanceId.value !== id) return; // stale
+      processes.value = resp.processes ?? [];
+    } catch {
+      if (enabled.value && instanceId.value === id) processes.value = [];
+    } finally {
+      if (enabled.value && instanceId.value === id) loading.value = false;
+    }
+  });
+  return { processes, loading };
+}
diff --git a/packages/api-client/src/ebpf.ts b/packages/api-client/src/ebpf.ts
index bfbe6cc..5400946 100644
--- a/packages/api-client/src/ebpf.ts
+++ b/packages/api-client/src/ebpf.ts
@@ -159,6 +159,19 @@ export interface ProcessTopologyResponse {
   error?: string;
 }
 
+/** A rover-monitored process on a service instance. Network profiling
+ *  targets the whole instance; this list confirms it has profilable
+ *  processes (OAP rejects a network task on an instance with none). */
+export interface NetworkProcess {
+  id: string;
+  name: string;
+}
+export interface NetworkProcessesResponse {
+  processes: NetworkProcess[];
+  reachable: boolean;
+  error?: string;
+}
+
 export interface NetworkProfilingSampling {
   uriRegex?: string;
   when4xx?: boolean;
diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts
index 8c6fadb..2ef5a1a 100644
--- a/packages/api-client/src/index.ts
+++ b/packages/api-client/src/index.ts
@@ -187,6 +187,8 @@ export type {
   ProcessNode,
   ProcessCall,
   ProcessTopologyResponse,
+  NetworkProcess,
+  NetworkProcessesResponse,
   NetworkProfilingSampling,
   NetworkProfilingCreateRequest,
   NetworkProfilingCreateResponse,

Reply via email to