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>
