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 23fcaaf080eb96cfc18e5c4233cbe3b6d3fe0a3d
Author: Wu Sheng <[email protected]>
AuthorDate: Fri Jun 26 23:28:50 2026 +0800

    feat(profiling): task-detail modals + consistent, honest task-creation 
gating
    
    pprof and async profiling tabs gain a task-detail + per-instance-logs modal 
(parity with trace profiling), reading OAP's existing task-progress logs.
    
    Task creation is now consistent: the New Task button is enabled on basic 
entity selection (never silently disabled on process availability) and always 
carries a tooltip; the create box surfaces the blocker with a visible reason 
and disables Create accordingly — eBPF on its real OAP couldProfiling signal, 
pprof/async when no instances are available. The network "no process" gate is 
removed: OAP exposes no upfront per-instance process-availability query (only 
queryPrepareCreateEBPFProfi [...]
    
    Also fixes two detail-modal bugs: a fast task switch could land one task's 
logs in another's modal (stale-fetch guard), and an open detail modal could 
linger across a layer/service change (now reset).
---
 .../profiling/AsyncProfilingTaskDetailModal.vue    | 177 +++++++++++++++++++++
 .../layer/profiling/LayerAsyncProfilingView.vue    |  69 +++++++-
 .../src/layer/profiling/LayerEBPFProfilingView.vue |  11 +-
 .../layer/profiling/LayerNetworkProfilingView.vue  |   1 +
 .../layer/profiling/LayerPprofProfilingView.vue    |  75 ++++++++-
 apps/ui/src/layer/profiling/NewEBPFTaskModal.vue   |  38 ++++-
 .../src/layer/profiling/PprofTaskDetailModal.vue   | 170 ++++++++++++++++++++
 7 files changed, 524 insertions(+), 17 deletions(-)

diff --git a/apps/ui/src/layer/profiling/AsyncProfilingTaskDetailModal.vue 
b/apps/ui/src/layer/profiling/AsyncProfilingTaskDetailModal.vue
new file mode 100644
index 0000000..ec8849d
--- /dev/null
+++ b/apps/ui/src/layer/profiling/AsyncProfilingTaskDetailModal.vue
@@ -0,0 +1,177 @@
+<!--
+  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.
+-->
+<!--
+  Read-only task-detail modal for the async-profiler (Java) tab — the
+  trace-profiling tab's counterpart. Shows the task's create parameters
+  (events, instance fan-out, duration, exec args) plus the per-instance
+  operation logs OAP fanned the task out to. Opening it must not disturb
+  the parent's selected-task / instance-filter state, so it is driven by
+  its own `task` ref in the parent. `fmtTime` is injected so the timestamp
+  format stays identical to the surrounding task list.
+-->
+<script setup lang="ts">
+import type { AsyncProfilingTask, AsyncProfilingProgressLog } from 
'@/api/client';
+import { useEscapeToClose } from '@/components/primitives/useEscapeToClose';
+
+const props = defineProps<{
+  task: AsyncProfilingTask | null;
+  serviceName: string | null;
+  logs: AsyncProfilingProgressLog[];
+  fmtTime: (ms: number) => string;
+}>();
+const emit = defineEmits<{
+  (e: 'close'): void;
+}>();
+
+useEscapeToClose(
+  () => props.task != null,
+  () => emit('close'),
+);
+</script>
+
+<template>
+  <div v-if="task" class="dlg-mask" @click.self="emit('close')">
+    <div class="dlg wide">
+      <div class="dlg-head">
+        <div>Async profile task detail</div>
+        <button class="x" @click="emit('close')">×</button>
+      </div>
+      <div class="dlg-body">
+        <dl class="kv">
+          <dt>Service</dt><dd>{{ serviceName ?? task.serviceId }}</dd>
+          <dt>Events</dt><dd>{{ task.events?.join(', ') || '—' }}</dd>
+          <dt>Instances</dt>
+          <dd>
+            {{ task.serviceInstanceIds?.length ?? 0 }} instance{{ 
(task.serviceInstanceIds?.length ?? 0) === 1 ? '' : 's' }}
+          </dd>
+          <dt>Create time</dt><dd>{{ fmtTime(task.createTime) }}</dd>
+          <dt>Duration</dt><dd>{{ task.duration }} sec</dd>
+          <template v-if="task.execArgs">
+            <dt>Exec args</dt><dd class="mono">{{ task.execArgs }}</dd>
+          </template>
+        </dl>
+        <div v-if="logs.length" class="logs">
+          <h5>Instance logs</h5>
+          <div v-for="(log, i) in logs" :key="i" class="log-line">
+            <span class="t-tag">{{ log.operationType }}</span>
+            <span class="muted">{{ log.instanceName }}</span>
+            <span>{{ fmtTime(log.operationTime) }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.muted {
+  color: var(--sw-fg-3);
+}
+.dlg-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.55);
+  z-index: 50;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.dlg {
+  background: var(--sw-bg-1);
+  border: 1px solid var(--sw-line);
+  border-radius: 6px;
+  width: 520px;
+  max-width: 92vw;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+}
+.dlg.wide {
+  width: 640px;
+}
+.dlg-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 14px;
+  border-bottom: 1px solid var(--sw-line);
+  background: var(--sw-bg-2);
+  font-weight: 600;
+  font-size: 12.5px;
+  color: var(--sw-fg-0);
+}
+.x {
+  background: transparent;
+  border: none;
+  color: var(--sw-fg-3);
+  font-size: 18px;
+  cursor: pointer;
+  line-height: 1;
+}
+.dlg-body {
+  padding: 14px;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+.kv {
+  display: grid;
+  grid-template-columns: 140px 1fr;
+  row-gap: 6px;
+  font-size: 11.5px;
+  margin: 0;
+}
+.kv dt {
+  color: var(--sw-fg-3);
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  font-size: 10px;
+  align-self: center;
+}
+.kv dd {
+  margin: 0;
+  color: var(--sw-fg-0);
+}
+.kv dd.mono {
+  font-family: var(--sw-mono);
+  word-break: break-all;
+}
+.logs h5 {
+  margin: 14px 0 6px;
+  font-size: 11px;
+  color: var(--sw-fg-1);
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+}
+.log-line {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  font-size: 11px;
+  padding: 4px 0;
+  border-bottom: 1px dotted var(--sw-line);
+}
+.t-tag {
+  font-size: 10px;
+  padding: 0 5px;
+  border-radius: 2px;
+  background: var(--sw-accent);
+  color: #fff;
+  font-weight: 600;
+}
+</style>
diff --git a/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue
index 334483c..67d164b 100644
--- a/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerAsyncProfilingView.vue
@@ -27,15 +27,18 @@
 import { computed, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { useLayerInstances } from '@/layer/useLayerInstances';
+import { useLayerServices } from '@/layer/useLayerServices';
 import { useSelectedService } from '@/layer/useSelectedService';
 import { bffClient } from '@/api/client';
 import type {
   AsyncProfilingEvent,
+  AsyncProfilingProgressLog,
   AsyncProfilingTask,
   AsyncProfilingTree,
   ProfileAnalyzationTree,
 } from '@/api/client';
 import ProfileFlameGraph from '@/layer/profiling/ProfileFlameGraph.vue';
+import AsyncProfilingTaskDetailModal from 
'@/layer/profiling/AsyncProfilingTaskDetailModal.vue';
 import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
 import Icon from '@/components/icons/Icon.vue';
 
@@ -43,12 +46,22 @@ const route = useRoute();
 const layerKey = computed(() => String(route.params.layerKey ?? ''));
 const { selectedId: serviceId } = useSelectedService();
 const instances = useLayerInstances(layerKey, serviceId);
+const { services: roster } = useLayerServices(layerKey);
+const serviceName = computed<string | null>(
+  () => roster.value.find((s) => s.id === serviceId.value)?.name ?? null,
+);
 
 const tasks = ref<AsyncProfilingTask[]>([]);
 const tasksError = ref<string | null>(null);
 const currentTask = ref<AsyncProfilingTask | null>(null);
 const tasksLoading = ref(false);
 
+// Track the operator's open task-detail modal independently of the
+// "current selected task" — opening the detail icon mustn't swap the
+// instance filter / flame graph out.
+const taskDetailFor = ref<AsyncProfilingTask | null>(null);
+const taskDetailLogs = ref<AsyncProfilingProgressLog[]>([]);
+
 const selectedInstances = ref<string[]>([]);
 const eventType = ref<string>('EXECUTION_SAMPLE');
 const tree = ref<AsyncProfilingTree | null>(null);
@@ -76,7 +89,10 @@ const EVENTS: AsyncProfilingEvent[] = ['CPU', 'ALLOC', 
'LOCK', 'WALL', 'CTIMER',
 
 watch(
   () => layerKey.value + '|' + (serviceId.value ?? ''),
-  () => void refreshTasks(),
+  () => {
+    taskDetailFor.value = null; // drop the detail modal on context change
+    void refreshTasks();
+  },
   { immediate: true },
 );
 
@@ -107,6 +123,19 @@ function syncInstancesFromTask(t: AsyncProfilingTask): 
void {
   else eventType.value = 'EXECUTION_SAMPLE';
 }
 
+async function openTaskDetail(t: AsyncProfilingTask, ev: Event): Promise<void> 
{
+  ev.stopPropagation();
+  taskDetailFor.value = t;
+  taskDetailLogs.value = [];
+  try {
+    const resp = await bffClient.asyncProfile.progress(t.id);
+    if (taskDetailFor.value?.id !== t.id) return; // stale: another task 
opened mid-fetch
+    taskDetailLogs.value = resp.progress?.logs ?? [];
+  } catch {
+    if (taskDetailFor.value?.id === t.id) taskDetailLogs.value = [];
+  }
+}
+
 async function runAnalyze(): Promise<void> {
   if (!currentTask.value) return;
   if (!selectedInstances.value.length) {
@@ -225,6 +254,7 @@ function instanceName(id: string): string {
           <button
             class="btn-new"
             :disabled="!serviceId"
+            :title="serviceId ? 'Create a new async profiling task' : 'Pick a 
service first'"
             @click="showNewTask = true"
           >+ New Task</button>
         </div>
@@ -245,6 +275,12 @@ function instanceName(id: string): string {
           <div class="row1">
             <span class="t-tag">{{ t.events?.join(',') }}</span>
             <span class="ep">{{ t.serviceInstanceIds?.length ?? 0 }} 
instance{{ (t.serviceInstanceIds?.length ?? 0) === 1 ? '' : 's' }}</span>
+            <button
+              type="button"
+              class="row-eye"
+              title="View task detail"
+              @click.stop="openTaskDetail(t, $event)"
+            >i</button>
           </div>
           <div class="row2">
             <span>{{ fmtTime(t.createTime) }}</span>
@@ -319,7 +355,7 @@ function instanceName(id: string): string {
               :class="{ on: newTask.instances.includes(i.id) }"
               @click="toggleInstance(i.id, 'new')"
             >{{ i.name }}</button>
-            <span v-if="!instances.instances.value.length" class="muted">No 
instances available.</span>
+            <span v-if="!instances.instances.value.length" class="muted">No 
instances available for this service — an async profiling task cannot be 
created.</span>
           </div>
         </div>
         <div class="field-row">
@@ -351,10 +387,23 @@ function instanceName(id: string): string {
       </div>
       <div class="dlg-foot">
         <button class="btn-secondary" @click="showNewTask = 
false">Cancel</button>
-        <button class="btn-primary" @click="submitNewTask">Create task</button>
+        <button
+          class="btn-primary"
+          :disabled="!newTask.instances.length || !newTask.events.length"
+          :title="!newTask.instances.length ? 'Select at least one instance' : 
!newTask.events.length ? 'Pick at least one event' : 'Create the async 
profiling task'"
+          @click="submitNewTask"
+        >Create task</button>
       </div>
     </div>
   </div>
+
+  <AsyncProfilingTaskDetailModal
+    :task="taskDetailFor"
+    :service-name="serviceName"
+    :logs="taskDetailLogs"
+    :fmt-time="fmtTime"
+    @close="taskDetailFor = null"
+  />
 </template>
 
 <style scoped>
@@ -487,6 +536,20 @@ function instanceName(id: string): string {
   text-overflow: ellipsis;
   white-space: nowrap;
 }
+.row-eye {
+  margin-left: auto;
+  background: transparent;
+  border: none;
+  color: var(--sw-fg-3);
+  cursor: pointer;
+  padding: 0;
+  font-style: italic;
+  font-weight: 600;
+  line-height: 1;
+}
+.row-eye:hover {
+  color: var(--sw-accent);
+}
 .muted {
   color: var(--sw-fg-3);
 }
diff --git a/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
index 36032f9..034c0bc 100644
--- a/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerEBPFProfilingView.vue
@@ -177,14 +177,8 @@ function onPickTask(t: EBPFTask): void {
           ><Icon name="refresh" :size="11" /></button>
           <button
             class="btn-new"
-            :disabled="!selectedId || !couldProfiling"
-            :title="
-              !selectedId
-                ? 'Pick a service'
-                : couldProfiling
-                  ? 'Create a new eBPF task'
-                  : 'OAP reports no profilable processes for this service'
-            "
+            :disabled="!selectedId"
+            :title="!selectedId ? 'Pick a service' : 'Create a new eBPF task'"
             @click="showNewTask = true"
           >+ New Task</button>
         </div>
@@ -305,6 +299,7 @@ function onPickTask(t: EBPFTask): void {
     v-model:show="showNewTask"
     :service-name="serviceName"
     :process-labels="allProcessLabels"
+    :could-profiling="couldProfiling"
     :error="newTaskError"
     @submit="onSubmitNewTask"
   />
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index babfcdb..10bf4ee 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -378,6 +378,7 @@ function fmtTime(ms: number): string {
           <button
             class="btn-new"
             :disabled="!selectedInstanceId"
+            :title="!selectedInstanceId ? 'Pick an instance first' : 'Create a 
new network profile task'"
             @click="showNewTask = true"
           >+ New Task</button>
         </div>
diff --git a/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue 
b/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue
index 419015b..0a3a4ce 100644
--- a/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerPprofProfilingView.vue
@@ -27,10 +27,17 @@
 import { computed, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { useLayerInstances } from '@/layer/useLayerInstances';
+import { useLayerServices } from '@/layer/useLayerServices';
 import { useSelectedService } from '@/layer/useSelectedService';
 import { bffClient } from '@/api/client';
-import type { PprofTask, PprofTree, ProfileAnalyzationTree } from 
'@/api/client';
+import type {
+  AsyncProfilingProgressLog,
+  PprofTask,
+  PprofTree,
+  ProfileAnalyzationTree,
+} from '@/api/client';
 import ProfileFlameGraph from '@/layer/profiling/ProfileFlameGraph.vue';
+import PprofTaskDetailModal from '@/layer/profiling/PprofTaskDetailModal.vue';
 import { useNewTaskPoll } from '@/layer/profiling/useNewTaskPoll';
 import Icon from '@/components/icons/Icon.vue';
 
@@ -38,6 +45,10 @@ const route = useRoute();
 const layerKey = computed(() => String(route.params.layerKey ?? ''));
 const { selectedId: serviceId } = useSelectedService();
 const instances = useLayerInstances(layerKey, serviceId);
+const { services } = useLayerServices(layerKey);
+const serviceName = computed<string | null>(
+  () => services.value.find((s) => s.id === serviceId.value)?.name ?? null,
+);
 
 const tasks = ref<PprofTask[]>([]);
 const tasksError = ref<string | null>(null);
@@ -49,6 +60,12 @@ const tree = ref<PprofTree | null>(null);
 const analyzeError = ref<string | null>(null);
 const analyzeLoading = ref(false);
 
+// Track the operator's open task-detail modal independently of the
+// "current selected task" — opening the detail icon mustn't reset the
+// selected instances / analyze result.
+const taskDetailFor = ref<PprofTask | null>(null);
+const taskDetailLogs = ref<AsyncProfilingProgressLog[]>([]);
+
 const showNewTask = ref(false);
 const newTask = reactive({
   instances: [] as string[],
@@ -80,7 +97,10 @@ const DURATION_OPTS = [
 
 watch(
   () => layerKey.value + '|' + (serviceId.value ?? ''),
-  () => void refreshTasks(),
+  () => {
+    taskDetailFor.value = null; // drop the detail modal on context change
+    void refreshTasks();
+  },
   { immediate: true },
 );
 
@@ -122,6 +142,19 @@ async function runAnalyze(): Promise<void> {
   }
 }
 
+async function openTaskDetail(t: PprofTask, ev: Event): Promise<void> {
+  ev.stopPropagation();
+  taskDetailFor.value = t;
+  taskDetailLogs.value = [];
+  try {
+    const resp = await bffClient.pprof.progress(t.id);
+    if (taskDetailFor.value?.id !== t.id) return; // stale: another task 
opened mid-fetch
+    taskDetailLogs.value = resp.progress?.logs ?? [];
+  } catch {
+    if (taskDetailFor.value?.id === t.id) taskDetailLogs.value = [];
+  }
+}
+
 const profileTrees = computed<ProfileAnalyzationTree[]>(() => {
   if (!tree.value) return [];
   return [
@@ -201,7 +234,7 @@ function instanceName(id: string): string {
             aria-label="Refresh task list"
             @click="refreshTasks"
           ><Icon name="refresh" :size="11" /></button>
-          <button class="btn-new" :disabled="!serviceId" @click="showNewTask = 
true">+ New Task</button>
+          <button class="btn-new" :disabled="!serviceId" :title="serviceId ? 
'Create a new pprof task' : 'Pick a service first'" @click="showNewTask = 
true">+ New Task</button>
         </div>
       </div>
       <div v-if="polling" class="poll-hint">Waiting for new task… ({{ 
pollRound }}/4)</div>
@@ -220,6 +253,12 @@ function instanceName(id: string): string {
           <div class="row1">
             <span class="t-tag">{{ t.events }}</span>
             <span class="ep">{{ t.serviceInstanceIds?.length ?? 0 }} 
instance{{ (t.serviceInstanceIds?.length ?? 0) === 1 ? '' : 's' }}</span>
+            <button
+              type="button"
+              class="row-eye"
+              title="View task detail"
+              @click.stop="openTaskDetail(t, $event)"
+            >i</button>
           </div>
           <div class="row2">
             <span>{{ fmtTime(t.createTime) }}</span>
@@ -283,7 +322,7 @@ function instanceName(id: string): string {
               :class="{ on: newTask.instances.includes(i.id) }"
               @click="toggleInstance(i.id, 'new')"
             >{{ i.name }}</button>
-            <span v-if="!instances.instances.value.length" class="muted">No 
instances available.</span>
+            <span v-if="!instances.instances.value.length" class="muted">No 
instances available for this service — a pprof task cannot be created.</span>
           </div>
         </div>
         <div class="field">
@@ -320,10 +359,23 @@ function instanceName(id: string): string {
       </div>
       <div class="dlg-foot">
         <button class="btn-secondary" @click="showNewTask = 
false">Cancel</button>
-        <button class="btn-primary" @click="submitNewTask">Create task</button>
+        <button
+          class="btn-primary"
+          :disabled="!newTask.instances.length || !newTask.event"
+          :title="!newTask.instances.length ? 'Select at least one instance' : 
!newTask.event ? 'Pick an event type' : 'Create the pprof task'"
+          @click="submitNewTask"
+        >Create task</button>
       </div>
     </div>
   </div>
+
+  <PprofTaskDetailModal
+    :task="taskDetailFor"
+    :service-name="serviceName"
+    :logs="taskDetailLogs"
+    :fmt-time="fmtTime"
+    @close="taskDetailFor = null"
+  />
 </template>
 
 <style scoped>
@@ -456,6 +508,19 @@ function instanceName(id: string): string {
   text-overflow: ellipsis;
   white-space: nowrap;
 }
+.row-eye {
+  margin-left: auto;
+  background: transparent;
+  border: none;
+  color: var(--sw-fg-3);
+  cursor: pointer;
+  padding: 0;
+  font-style: italic;
+  line-height: 1;
+}
+.row-eye:hover {
+  color: var(--sw-accent);
+}
 .muted {
   color: var(--sw-fg-3);
 }
diff --git a/apps/ui/src/layer/profiling/NewEBPFTaskModal.vue 
b/apps/ui/src/layer/profiling/NewEBPFTaskModal.vue
index a6b0cba..220d59e 100644
--- a/apps/ui/src/layer/profiling/NewEBPFTaskModal.vue
+++ b/apps/ui/src/layer/profiling/NewEBPFTaskModal.vue
@@ -31,6 +31,7 @@ const props = defineProps<{
   show: boolean;
   serviceName: string | null;
   processLabels: string[];
+  couldProfiling: boolean;
   error: string | null;
 }>();
 const emit = defineEmits<{
@@ -91,6 +92,15 @@ function submit(): void {
         <button class="x" @click="close">×</button>
       </div>
       <div class="dlg-body">
+        <div v-if="!couldProfiling" class="dlg-warn">
+          <strong>OAP reports no profilable processes for this 
service.</strong>
+          <span>A new task can't run until OAP sees an eBPF-profilable 
process. Check that:</span>
+          <ul>
+            <li>instances are running and instrumented;</li>
+            <li>eBPF collection is enabled in OAP;</li>
+            <li>the processes support eBPF profiling.</li>
+          </ul>
+        </div>
         <div class="field">
           <label>Process labels (filter; leave empty = all)</label>
           <div class="chip-row">
@@ -142,7 +152,12 @@ function submit(): void {
       </div>
       <div class="dlg-foot">
         <button class="btn-secondary" @click="close">Cancel</button>
-        <button class="btn-primary" @click="submit">Create task</button>
+        <button
+          class="btn-primary"
+          :disabled="!couldProfiling"
+          :title="couldProfiling ? '' : 'OAP reports no profilable processes 
for this service'"
+          @click="submit"
+        >Create task</button>
       </div>
     </div>
   </div>
@@ -307,4 +322,25 @@ function submit(): void {
   color: var(--sw-err);
   font-size: 11px;
 }
+.dlg-warn {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  padding: 10px 12px;
+  font-size: 11.5px;
+  color: var(--sw-fg-1);
+  background: var(--sw-bg-2);
+  border: 1px solid var(--sw-line);
+  border-left: 3px solid var(--sw-err);
+  border-radius: 4px;
+}
+.dlg-warn strong {
+  color: var(--sw-err);
+  font-weight: 600;
+}
+.dlg-warn ul {
+  margin: 0;
+  padding-left: 18px;
+  color: var(--sw-fg-2);
+}
 </style>
diff --git a/apps/ui/src/layer/profiling/PprofTaskDetailModal.vue 
b/apps/ui/src/layer/profiling/PprofTaskDetailModal.vue
new file mode 100644
index 0000000..f86a3c9
--- /dev/null
+++ b/apps/ui/src/layer/profiling/PprofTaskDetailModal.vue
@@ -0,0 +1,170 @@
+<!--
+  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.
+-->
+<!--
+  Read-only task-detail modal for the pprof (Go) profiling tab: the task's
+  create parameters plus the per-instance operation logs OAP fanned the task
+  out to. Opening it must not disturb the parent's selected-task / analyze
+  state, so it is driven by its own `task` ref in the parent. `fmtTime` is
+  injected so the timestamp format stays identical to the surrounding task
+  list. Mirrors TraceProfileTaskDetailModal, but pprof tasks carry an event
+  type + optional duration / dumpPeriod instead of endpoint + thresholds.
+-->
+<script setup lang="ts">
+import type { PprofTask, AsyncProfilingProgressLog } from '@/api/client';
+import { useEscapeToClose } from '@/components/primitives/useEscapeToClose';
+
+const props = defineProps<{
+  task: PprofTask | null;
+  serviceName: string | null;
+  logs: AsyncProfilingProgressLog[];
+  fmtTime: (ms: number) => string;
+}>();
+const emit = defineEmits<{
+  (e: 'close'): void;
+}>();
+
+useEscapeToClose(
+  () => props.task != null,
+  () => emit('close'),
+);
+</script>
+
+<template>
+  <div v-if="task" class="dlg-mask" @click.self="emit('close')">
+    <div class="dlg wide">
+      <div class="dlg-head">
+        <div>pprof task detail</div>
+        <button class="x" @click="emit('close')">×</button>
+      </div>
+      <div class="dlg-body">
+        <dl class="kv">
+          <dt>Service</dt><dd>{{ serviceName ?? task.serviceId }}</dd>
+          <dt>Instances</dt><dd>{{ task.serviceInstanceIds?.length ?? 0 }}</dd>
+          <dt>Event</dt><dd>{{ task.events }}</dd>
+          <dt>Create time</dt><dd>{{ fmtTime(task.createTime) }}</dd>
+          <dt v-if="task.duration != null">Duration</dt>
+          <dd v-if="task.duration != null">{{ task.duration }} min</dd>
+          <dt v-if="task.dumpPeriod != null">Dump period</dt>
+          <dd v-if="task.dumpPeriod != null">{{ task.dumpPeriod }}</dd>
+        </dl>
+        <div v-if="logs.length" class="logs">
+          <h5>Instance logs</h5>
+          <div v-for="(log, i) in logs" :key="i" class="log-line">
+            <span class="t-tag">{{ log.operationType }}</span>
+            <span class="muted">{{ log.instanceName }}</span>
+            <span>{{ fmtTime(log.operationTime) }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.muted {
+  color: var(--sw-fg-3);
+}
+.dlg-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.55);
+  z-index: 50;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.dlg {
+  background: var(--sw-bg-1);
+  border: 1px solid var(--sw-line);
+  border-radius: 6px;
+  width: 520px;
+  max-width: 92vw;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+}
+.dlg.wide {
+  width: 640px;
+}
+.dlg-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 14px;
+  border-bottom: 1px solid var(--sw-line);
+  background: var(--sw-bg-2);
+  font-weight: 600;
+  font-size: 12.5px;
+  color: var(--sw-fg-0);
+}
+.x {
+  background: transparent;
+  border: none;
+  color: var(--sw-fg-3);
+  font-size: 18px;
+  cursor: pointer;
+  line-height: 1;
+}
+.dlg-body {
+  padding: 14px;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+.kv {
+  display: grid;
+  grid-template-columns: 140px 1fr;
+  row-gap: 6px;
+  font-size: 11.5px;
+  margin: 0;
+}
+.kv dt {
+  color: var(--sw-fg-3);
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  font-size: 10px;
+  align-self: center;
+}
+.kv dd {
+  margin: 0;
+  color: var(--sw-fg-0);
+}
+.logs h5 {
+  margin: 14px 0 6px;
+  font-size: 11px;
+  color: var(--sw-fg-1);
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+}
+.log-line {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  font-size: 11px;
+  padding: 4px 0;
+  border-bottom: 1px dotted var(--sw-line);
+}
+.t-tag {
+  font-size: 10px;
+  padding: 0 5px;
+  border-radius: 2px;
+  background: #6c5cd9;
+  color: #fff;
+  font-weight: 600;
+}
+</style>

Reply via email to